aboutsummaryrefslogtreecommitdiff
path: root/rpki/gui
diff options
context:
space:
mode:
Diffstat (limited to 'rpki/gui')
-rw-r--r--rpki/gui/__init__.py0
-rw-r--r--rpki/gui/api/__init__.py0
-rw-r--r--rpki/gui/api/urls.py22
-rw-r--r--rpki/gui/app/TODO60
-rw-r--r--rpki/gui/app/__init__.py0
-rw-r--r--rpki/gui/app/admin.py0
-rw-r--r--rpki/gui/app/check_expired.py209
-rw-r--r--rpki/gui/app/forms.py442
-rw-r--r--rpki/gui/app/glue.py132
-rw-r--r--rpki/gui/app/migrations/0001_initial.py192
-rw-r--r--rpki/gui/app/migrations/0002_auto__add_field_resourcecert_conf.py117
-rw-r--r--rpki/gui/app/migrations/0003_set_conf_from_parent.py116
-rw-r--r--rpki/gui/app/migrations/0004_auto__chg_field_resourcecert_conf.py115
-rw-r--r--rpki/gui/app/migrations/0005_auto__chg_field_resourcecert_parent.py115
-rw-r--r--rpki/gui/app/migrations/0006_add_conf_acl.py168
-rw-r--r--rpki/gui/app/migrations/0007_default_acls.py165
-rw-r--r--rpki/gui/app/migrations/0008_add_alerts.py176
-rw-r--r--rpki/gui/app/migrations/__init__.py0
-rw-r--r--rpki/gui/app/models.py420
-rwxr-xr-xrpki/gui/app/range_list.py252
-rw-r--r--rpki/gui/app/static/css/bootstrap.min.css9
-rw-r--r--rpki/gui/app/static/img/glyphicons-halflings-white.pngbin0 -> 8777 bytes
-rw-r--r--rpki/gui/app/static/img/glyphicons-halflings.pngbin0 -> 12799 bytes
-rw-r--r--rpki/gui/app/static/img/sui-riu.icobin0 -> 6126 bytes
-rw-r--r--rpki/gui/app/static/js/bootstrap.min.js6
-rw-r--r--rpki/gui/app/static/js/jquery-1.8.3.min.js2
-rw-r--r--rpki/gui/app/templates/404.html11
-rw-r--r--rpki/gui/app/templates/500.html11
-rw-r--r--rpki/gui/app/templates/app/alert_confirm_clear.html21
-rw-r--r--rpki/gui/app/templates/app/alert_confirm_delete.html17
-rw-r--r--rpki/gui/app/templates/app/alert_detail.html31
-rw-r--r--rpki/gui/app/templates/app/alert_list.html31
-rw-r--r--rpki/gui/app/templates/app/app_base.html31
-rw-r--r--rpki/gui/app/templates/app/app_confirm_delete.html21
-rw-r--r--rpki/gui/app/templates/app/app_form.html19
-rw-r--r--rpki/gui/app/templates/app/bootstrap_form.html26
-rw-r--r--rpki/gui/app/templates/app/child_detail.html48
-rw-r--r--rpki/gui/app/templates/app/client_detail.html25
-rw-r--r--rpki/gui/app/templates/app/client_list.html22
-rw-r--r--rpki/gui/app/templates/app/conf_empty.html17
-rw-r--r--rpki/gui/app/templates/app/conf_list.html17
-rw-r--r--rpki/gui/app/templates/app/dashboard.html230
-rw-r--r--rpki/gui/app/templates/app/ghostbuster_confirm_delete.html20
-rw-r--r--rpki/gui/app/templates/app/ghostbusterrequest_detail.html64
-rw-r--r--rpki/gui/app/templates/app/import_resource_form.html9
-rw-r--r--rpki/gui/app/templates/app/object_confirm_delete.html21
-rw-r--r--rpki/gui/app/templates/app/parent_detail.html67
-rw-r--r--rpki/gui/app/templates/app/pubclient_list.html10
-rw-r--r--rpki/gui/app/templates/app/repository_detail.html19
-rw-r--r--rpki/gui/app/templates/app/resource_holder_list.html37
-rw-r--r--rpki/gui/app/templates/app/roa_detail.html40
-rw-r--r--rpki/gui/app/templates/app/roarequest_confirm_delete.html59
-rw-r--r--rpki/gui/app/templates/app/roarequest_confirm_form.html60
-rw-r--r--rpki/gui/app/templates/app/roarequest_confirm_multi_form.html66
-rw-r--r--rpki/gui/app/templates/app/roarequest_form.html50
-rw-r--r--rpki/gui/app/templates/app/roarequest_multi_form.html28
-rw-r--r--rpki/gui/app/templates/app/route_detail.html58
-rw-r--r--rpki/gui/app/templates/app/routes_view.html55
-rw-r--r--rpki/gui/app/templates/app/user_list.html37
-rw-r--r--rpki/gui/app/templates/base.html63
-rw-r--r--rpki/gui/app/templates/registration/login.html25
-rw-r--r--rpki/gui/app/templatetags/__init__.py0
-rw-r--r--rpki/gui/app/templatetags/app_extras.py58
-rw-r--r--rpki/gui/app/templatetags/bootstrap_pager.py55
-rw-r--r--rpki/gui/app/timestamp.py25
-rw-r--r--rpki/gui/app/urls.py81
-rw-r--r--rpki/gui/app/views.py1314
-rw-r--r--rpki/gui/cacheview/__init__.py0
-rw-r--r--rpki/gui/cacheview/forms.py51
-rw-r--r--rpki/gui/cacheview/misc.py31
-rw-r--r--rpki/gui/cacheview/models.py237
-rw-r--r--rpki/gui/cacheview/templates/cacheview/addressrange_detail.html18
-rw-r--r--rpki/gui/cacheview/templates/cacheview/cacheview_base.html10
-rw-r--r--rpki/gui/cacheview/templates/cacheview/cert_detail.html105
-rw-r--r--rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html13
-rw-r--r--rpki/gui/cacheview/templates/cacheview/global_summary.html26
-rw-r--r--rpki/gui/cacheview/templates/cacheview/query_result.html21
-rw-r--r--rpki/gui/cacheview/templates/cacheview/roa_detail.html18
-rw-r--r--rpki/gui/cacheview/templates/cacheview/search_form.html17
-rw-r--r--rpki/gui/cacheview/templates/cacheview/search_result.html42
-rw-r--r--rpki/gui/cacheview/templates/cacheview/signedobject_detail.html58
-rw-r--r--rpki/gui/cacheview/tests.py23
-rw-r--r--rpki/gui/cacheview/urls.py32
-rw-r--r--rpki/gui/cacheview/util.py432
-rw-r--r--rpki/gui/cacheview/views.py172
-rw-r--r--rpki/gui/decorators.py31
-rw-r--r--rpki/gui/default_settings.py171
-rw-r--r--rpki/gui/models.py150
-rw-r--r--rpki/gui/routeview/__init__.py0
-rw-r--r--rpki/gui/routeview/api.py69
-rw-r--r--rpki/gui/routeview/models.py81
-rw-r--r--rpki/gui/routeview/util.py236
-rw-r--r--rpki/gui/script_util.py43
-rw-r--r--rpki/gui/urls.py36
-rw-r--r--rpki/gui/views.py30
95 files changed, 7770 insertions, 0 deletions
diff --git a/rpki/gui/__init__.py b/rpki/gui/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/__init__.py
diff --git a/rpki/gui/api/__init__.py b/rpki/gui/api/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/api/__init__.py
diff --git a/rpki/gui/api/urls.py b/rpki/gui/api/urls.py
new file mode 100644
index 00000000..8c9d824c
--- /dev/null
+++ b/rpki/gui/api/urls.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.conf.urls.defaults import *
+from rpki.gui.routeview.api import route_list
+
+urlpatterns = patterns('',
+ (r'^v1/route/$', route_list),
+)
diff --git a/rpki/gui/app/TODO b/rpki/gui/app/TODO
new file mode 100644
index 00000000..b7136397
--- /dev/null
+++ b/rpki/gui/app/TODO
@@ -0,0 +1,60 @@
+Use RequestContext (helper function for render_to_response) and a default
+list of context processors for the generic functions
+
+Teach cert_delete about children, conf*, parent* to say what the ramifications
+of deleting a cert are.
+
+Teach cert form about file upload
+
+Redirect /accounts/profile/ to /dashboard/
+
+Teach dashboard view about looking up resources from parent.
+There are 3 types of resources:
+- Ones we've accepted and match
+- Ones we've accepted but don't match
+ - two subtypes:
+ * the parent is now giving us a superset of what they used to.
+ This is relatively easily handled by keeping the subdivisions
+ we've made and just making the superset resource the new parent
+ of the existing resource (e.g., we had accepted 18.5.0.0/16 and
+ they're now giving us 18.0.0.0/8)
+ * the parent is now giving us a subset (including none) of what they
+ used to. Two sub-cases:
+ - The part that they took away is neither delegated nor roa'd.
+ - The part that they took away is either delegated or roa'd or both.
+- Ones we haven't accepted yet
+
+The roa needs to learn to handle its prefix children. It may need to
+create the covering set of prefixes for an address range.
+
+Un<something>'d resources are:
+what we've gotten from our parent:
+models.AddressRange.objects.filter(from_parent=myconf.pk)
+minus what we've given to our children or issued roas for
+models.AddressRange.objects.filter(child__conf=myconf.pk)
+models.AddressRange.objects.filter(roa__conf=myconf.pk)
+or
+>>> from django.db.models import Q
+>>> models.AddressRange.objects.filter( Q(child__conf=myconf.pk) |
+ Q(roa__conf=myconf.pk) )
+
+
+and of course the ASN one is easier:
+models.Asn.objects.filter(from_parent=myconf.pk)
+minus what we've given to our children
+models.Asn.objects.filter(child__conf=myconf.pk)
+
+look in
+rpki/resource_set.py
+
+
+Adding a handle / resource-holding entity / "conf":
+- upload the <identity> that we've generated and are sending to the parent
+
+Adding a parent:
+- upload the <parent> that he sent me
+ (keep things open to the parent uploading this directly to the web interface)
+
+Adding a child:
+- upload the <identity> that he sent me
+
diff --git a/rpki/gui/app/__init__.py b/rpki/gui/app/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/app/__init__.py
diff --git a/rpki/gui/app/admin.py b/rpki/gui/app/admin.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/app/admin.py
diff --git a/rpki/gui/app/check_expired.py b/rpki/gui/app/check_expired.py
new file mode 100644
index 00000000..fcf5ecae
--- /dev/null
+++ b/rpki/gui/app/check_expired.py
@@ -0,0 +1,209 @@
+# Copyright (C) 2012, 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+__all__ = ('notify_expired', 'NetworkError')
+
+import sys
+import socket
+from cStringIO import StringIO
+import logging
+import datetime
+
+from rpki.gui.cacheview.models import Cert
+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
+
+from django.core.mail import send_mail
+
+logger = logging.getLogger(__name__)
+expire_time = 0 # set by notify_expired()
+now = 0
+
+
+def check_cert(handle, p, errs):
+ """Check the expiration date on the X.509 certificates in each element of
+ the list.
+
+ The displayed object name defaults to the class name, but can be overridden
+ using the `object_name` argument.
+
+ """
+ t = p.certificate.getNotAfter()
+ if t <= expire_time:
+ e = 'expired' if t <= now else 'will expire'
+ errs.write("%(handle)s's %(type)s %(desc)s %(expire)s on %(date)s\n" % {
+ 'handle': handle, 'type': p.__class__.__name__, 'desc': str(p),
+ 'expire': e, 'date': t})
+
+
+def check_cert_list(handle, x, errs):
+ for p in x:
+ check_cert(handle, p, errs)
+
+
+def check_expire(conf, errs):
+ # get certs for `handle'
+ cert_set = ResourceCert.objects.filter(conf=conf)
+ for cert in cert_set:
+ # look up cert in cacheview db
+ obj_set = Cert.objects.filter(repo__uri=cert.uri)
+ if not obj_set:
+ # since the <list_received_resources/> output is cached, this can
+ # occur if the cache is out of date as well..
+ errs.write("Unable to locate rescert in rcynic cache: handle=%s uri=%s not_after=%s\n" % (conf.handle, cert.uri, cert.not_after))
+ continue
+ obj = obj_set[0]
+ msg = []
+ expired = False
+ for n, c in enumerate(obj.cert_chain):
+ if c.not_after <= expire_time:
+ expired = True
+ f = '*'
+ else:
+ f = ' '
+ msg.append("%s [%d] uri=%s ski=%s name=%s expires=%s" % (f, n, c.repo.uri, c.keyid, c.name, c.not_after))
+
+ # find ghostbuster records attached to this cert
+ for gbr in c.ghostbusters.all():
+ info = []
+ for s in ('full_name', 'organization', 'email_address', 'telephone'):
+ t = getattr(gbr, s, None)
+ if t:
+ info.append(t)
+
+ msg.append(" Contact: " + ", ".join(info))
+
+ if expired:
+ errs.write("%s's rescert from parent %s will expire soon:\n" % (
+ conf.handle,
+ # parent is None for the root cert
+ cert.parent.handle if cert.parent else 'self'
+ ))
+ errs.write("Certificate chain:\n")
+ errs.write("\n".join(msg))
+ errs.write("\n")
+
+
+def check_child_certs(conf, errs):
+ """Fetch the list of published objects from rpkid, and inspect the issued
+ resource certs (uri ending in .cer).
+
+ """
+ z = Zookeeper(handle=conf.handle)
+ req = list_published_objects_elt.make_pdu(action="list",
+ tag="list_published_objects",
+ self_handle=conf.handle)
+ pdus = z.call_rpkid(req)
+ for pdu in pdus:
+ if isinstance(pdu, report_error_elt):
+ logger.error("rpkid reported an error: %s" % pdu.error_code)
+ elif isinstance(pdu, list_published_objects_elt):
+ if pdu.uri.endswith('.cer'):
+ cert = X509()
+ cert.set(Base64=pdu.obj)
+ t = cert.getNotAfter()
+ if t <= expire_time:
+ e = 'expired' if t <= now else 'will expire'
+ errs.write("%(handle)s's rescert for Child %(child)s %(expire)s on %(date)s uri=%(uri)s subject=%(subject)s\n" % {
+ 'handle': conf.handle,
+ 'child': pdu.child_handle,
+ 'uri': pdu.uri,
+ 'subject': cert.getSubject(),
+ 'expire': e,
+ 'date': t})
+
+
+class NetworkError(Exception):
+ pass
+
+
+def notify_expired(expire_days=14, from_email=None):
+ """Send email notificates about impending expirations of resource
+ and BPKI certificates.
+
+ expire_days: the number of days ahead of today to warn
+
+ from_email: set the From: address for the email
+
+ """
+ global expire_time # so i don't have to pass it around
+ global now
+
+ now = datetime.datetime.utcnow()
+ expire_time = now + datetime.timedelta(expire_days)
+
+ # this is not exactly right, since we have no way of knowing what the
+ # vhost for the web portal running on this machine is
+ host = socket.getfqdn()
+ if not from_email:
+ from_email = 'root@' + host
+
+ # Ensure that the rcynic and routeviews data has been updated recently
+ # The QuerySet is created here so that it will be cached and reused on each
+ # iteration of the loop below
+ t = now - datetime.timedelta(hours=12) # 12 hours
+ stale_timestamps = Timestamp.objects.filter(ts__lte=t)
+
+ # if not arguments are given, query all resource holders
+ qs = Conf.objects.all()
+
+ # check expiration of certs for all handles managed by the web portal
+ for h in qs:
+ # Force cache update since several checks require fresh data
+ try:
+ list_received_resources(sys.stdout, h)
+ except socket.error as e:
+ raise NetworkError('Error while talking to rpkid: %s' % e)
+
+ errs = StringIO()
+
+ # Warn the resource holder admins when data may be out of date
+ if stale_timestamps:
+ errs.write('Warning! Stale data from external sources.\n')
+ errs.write('data source : last import\n')
+ for obj in stale_timestamps:
+ errs.write('%-15s: %s\n' % (obj.name, obj.ts))
+ errs.write('\n')
+
+ check_cert(h.handle, h, errs)
+
+ # HostedCA is the ResourceHolderCA cross certified under ServerCA, so
+ # check the ServerCA expiration date as well
+ check_cert(h.handle, h.hosted_by, errs)
+ check_cert(h.handle, h.hosted_by.issuer, errs)
+
+ check_cert_list(h.handle, h.bscs.all(), errs)
+ check_cert_list(h.handle, h.parents.all(), errs)
+ check_cert_list(h.handle, h.children.all(), errs)
+ check_cert_list(h.handle, h.repositories.all(), errs)
+
+ check_expire(h, errs)
+ check_child_certs(h, errs)
+
+ # if there was output, display it now
+ s = errs.getvalue()
+ if s:
+ logger.info(s)
+
+ 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/rpki/gui/app/forms.py b/rpki/gui/app/forms.py
new file mode 100644
index 00000000..20ce4a07
--- /dev/null
+++ b/rpki/gui/app/forms.py
@@ -0,0 +1,442 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+
+from django.contrib.auth.models import User
+from django import forms
+from rpki.resource_set import (resource_range_as, resource_range_ip)
+from rpki.gui.app import models
+from rpki.exceptions import BadIPResource
+from rpki.POW import IPAddress
+
+
+class AddConfForm(forms.Form):
+ handle = forms.CharField(required=True,
+ help_text='your handle for your rpki instance')
+ run_rpkid = forms.BooleanField(required=False, initial=True,
+ label='Run rpkid?',
+ help_text='do you want to run your own instance of rpkid?')
+ rpkid_server_host = forms.CharField(initial='rpkid.example.org',
+ label='rpkid hostname',
+ help_text='publicly visible hostname for your rpkid instance')
+ rpkid_server_port = forms.IntegerField(initial=4404,
+ label='rpkid port')
+ run_pubd = forms.BooleanField(required=False, initial=False,
+ label='Run pubd?',
+ help_text='do you want to run your own instance of pubd?')
+ pubd_server_host = forms.CharField(initial='pubd.example.org',
+ label='pubd hostname',
+ help_text='publicly visible hostname for your pubd instance')
+ pubd_server_port = forms.IntegerField(initial=4402, label='pubd port')
+ pubd_contact_info = forms.CharField(initial='repo-man@rpki.example.org',
+ label='Pubd contact',
+ help_text='email address for the operator of your pubd instance')
+
+
+class GhostbusterRequestForm(forms.ModelForm):
+ """
+ Generate a ModelForm with the subset of parents for the current
+ resource handle.
+ """
+ # override default form field
+ parent = forms.ModelChoiceField(queryset=None, required=False,
+ help_text='Specify specific parent, or none for all parents')
+
+ #override
+ issuer = forms.ModelChoiceField(queryset=None, widget=forms.HiddenInput)
+
+ def __init__(self, *args, **kwargs):
+ conf = kwargs.pop('conf')
+ # override initial value for conf in case user tries to alter it
+ initial = kwargs.setdefault('initial', {})
+ initial['issuer'] = conf
+ super(GhostbusterRequestForm, self).__init__(*args, **kwargs)
+ self.fields['parent'].queryset = conf.parents.all()
+ self.fields['issuer'].queryset = models.Conf.objects.filter(pk=conf.pk)
+
+ class Meta:
+ model = models.GhostbusterRequest
+ exclude = ('vcard', 'given_name', 'family_name', 'additional_name',
+ 'honorific_prefix', 'honorific_suffix')
+
+ def clean(self):
+ email = self.cleaned_data.get('email_address')
+ postal = self.cleaned_data.get('postal_address')
+ telephone = self.cleaned_data.get('telephone')
+ if not any([email, postal, telephone]):
+ raise forms.ValidationError(
+ 'One of telephone, email or postal address must be specified')
+
+ return self.cleaned_data
+
+
+class ImportForm(forms.Form):
+ """Form used for uploading parent/child identity xml files."""
+ handle = forms.CharField(required=False,
+ widget=forms.TextInput(attrs={'class': 'xlarge'}),
+ help_text='Optional. Your name for this entity, or blank to accept name in XML')
+ xml = forms.FileField(label='XML file')
+
+
+class ImportRepositoryForm(forms.Form):
+ handle = forms.CharField(max_length=30, required=False,
+ label='Parent Handle',
+ help_text='Optional. Must be specified if you use a different name for this parent')
+ xml = forms.FileField(label='XML file')
+
+
+class ImportClientForm(forms.Form):
+ """Form used for importing publication client requests."""
+ xml = forms.FileField(label='XML file')
+
+
+class ImportCSVForm(forms.Form):
+ csv = forms.FileField(label='CSV file')
+
+
+class UserCreateForm(forms.Form):
+ username = forms.CharField(max_length=30)
+ email = forms.CharField(max_length=30,
+ help_text='email address for new user')
+ password = forms.CharField(widget=forms.PasswordInput)
+ password2 = forms.CharField(widget=forms.PasswordInput,
+ label='Confirm Password')
+ resource_holders = forms.ModelMultipleChoiceField(
+ queryset=models.Conf.objects.all(),
+ help_text='allowed to manage these resource holders'
+
+ )
+
+ def clean_username(self):
+ username = self.cleaned_data.get('username')
+ if User.objects.filter(username=username).exists():
+ raise forms.ValidationError('user already exists')
+ return username
+
+ def clean(self):
+ p1 = self.cleaned_data.get('password')
+ p2 = self.cleaned_data.get('password2')
+ if p1 != p2:
+ raise forms.ValidationError('passwords do not match')
+ return self.cleaned_data
+
+
+class UserEditForm(forms.Form):
+ """Form for editing a user."""
+ email = forms.CharField()
+ pw = forms.CharField(widget=forms.PasswordInput, label='Password',
+ required=False)
+ pw2 = forms.CharField(widget=forms.PasswordInput, label='Confirm password',
+ required=False)
+ resource_holders = forms.ModelMultipleChoiceField(
+ queryset=models.Conf.objects.all(),
+ help_text='allowed to manage these resource holders'
+ )
+
+ def clean(self):
+ p1 = self.cleaned_data.get('pw')
+ p2 = self.cleaned_data.get('pw2')
+ if p1 != p2:
+ raise forms.ValidationError('Passwords do not match')
+ return self.cleaned_data
+
+
+class ROARequest(forms.Form):
+ """Form for entering a ROA request.
+
+ Handles both IPv4 and IPv6."""
+
+ prefix = forms.CharField(
+ widget=forms.TextInput(attrs={
+ 'autofocus': 'true', 'placeholder': 'Prefix',
+ 'class': 'span4'
+ })
+ )
+ max_prefixlen = forms.CharField(
+ required=False,
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'Max len',
+ 'class': 'span1'
+ })
+ )
+ asn = forms.IntegerField(
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'ASN',
+ 'class': 'span1'
+ })
+ )
+ confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ """Takes an optional `conf` keyword argument specifying the user that
+ is creating the ROAs. It is used for validating that the prefix the
+ user entered is currently allocated to that user.
+
+ """
+ conf = kwargs.pop('conf', None)
+ kwargs['auto_id'] = False
+ super(ROARequest, self).__init__(*args, **kwargs)
+ self.conf = conf
+ self.inline = True
+ self.use_table = False
+
+ def _as_resource_range(self):
+ """Convert the prefix in the form to a
+ rpki.resource_set.resource_range_ip object.
+
+ If there is no mask provided, assume the closest classful mask.
+
+ """
+ prefix = self.cleaned_data.get('prefix')
+ if '/' not in prefix:
+ p = IPAddress(prefix)
+
+ # determine the first nonzero bit starting from the lsb and
+ # subtract from the address size to find the closest classful
+ # mask that contains this single address
+ prefixlen = 0
+ while (p != 0) and (p & 1) == 0:
+ prefixlen = prefixlen + 1
+ p = p >> 1
+ mask = p.bits - (8 * (prefixlen / 8))
+ prefix = prefix + '/' + str(mask)
+
+ return resource_range_ip.parse_str(prefix)
+
+ def clean_asn(self):
+ value = self.cleaned_data.get('asn')
+ if value < 0:
+ raise forms.ValidationError('AS must be a positive value or 0')
+ return value
+
+ def clean_prefix(self):
+ try:
+ r = self._as_resource_range()
+ except:
+ raise forms.ValidationError('invalid prefix')
+
+ manager = models.ResourceRangeAddressV4 if r.version == 4 else models.ResourceRangeAddressV6
+ if not manager.objects.filter(cert__conf=self.conf,
+ prefix_min__lte=r.min,
+ prefix_max__gte=r.max).exists():
+ raise forms.ValidationError('prefix is not allocated to you')
+ return str(r)
+
+ def clean_max_prefixlen(self):
+ v = self.cleaned_data.get('max_prefixlen')
+ if v:
+ if v[0] == '/':
+ v = v[1:] # allow user to specify /24
+ try:
+ if int(v) < 0:
+ raise forms.ValidationError('max prefix length must be positive or 0')
+ except ValueError:
+ raise forms.ValidationError('invalid integer value')
+ return v
+
+ def clean(self):
+ if 'prefix' in self.cleaned_data:
+ r = self._as_resource_range()
+ max_prefixlen = self.cleaned_data.get('max_prefixlen')
+ max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen()
+ if max_prefixlen < r.prefixlen():
+ raise forms.ValidationError(
+ 'max prefix length must be greater than or equal to the prefix length')
+ if max_prefixlen > r.min.bits:
+ raise forms.ValidationError, \
+ 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.min.bits)
+ self.cleaned_data['max_prefixlen'] = str(max_prefixlen)
+ return self.cleaned_data
+
+
+class ROARequestConfirm(forms.Form):
+ asn = forms.IntegerField(widget=forms.HiddenInput)
+ prefix = forms.CharField(widget=forms.HiddenInput)
+ max_prefixlen = forms.IntegerField(widget=forms.HiddenInput)
+
+ def clean_asn(self):
+ value = self.cleaned_data.get('asn')
+ if value < 0:
+ raise forms.ValidationError('AS must be a positive value or 0')
+ return value
+
+ def clean_prefix(self):
+ try:
+ r = resource_range_ip.parse_str(self.cleaned_data.get('prefix'))
+ except BadIPResource:
+ raise forms.ValidationError('invalid prefix')
+ return str(r)
+
+ def clean(self):
+ try:
+ r = resource_range_ip.parse_str(self.cleaned_data.get('prefix'))
+ if r.prefixlen() > self.cleaned_data.get('max_prefixlen'):
+ raise forms.ValidationError('max length is smaller than mask')
+ except BadIPResource:
+ pass
+ return self.cleaned_data
+
+
+class AddASNForm(forms.Form):
+ """
+ Returns a forms.Form subclass which verifies that the entered ASN range
+ does not overlap with a previous allocation to the specified child, and
+ that the ASN range is within the range allocated to the parent.
+
+ """
+
+ asns = forms.CharField(
+ label='ASNs',
+ help_text='single ASN or range',
+ widget=forms.TextInput(attrs={'autofocus': 'true'})
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child')
+ super(AddASNForm, self).__init__(*args, **kwargs)
+
+ def clean_asns(self):
+ try:
+ r = resource_range_as.parse_str(self.cleaned_data.get('asns'))
+ except:
+ raise forms.ValidationError('invalid AS or range')
+
+ if not models.ResourceRangeAS.objects.filter(
+ cert__conf=self.child.issuer,
+ min__lte=r.min,
+ max__gte=r.max).exists():
+ raise forms.ValidationError('AS or range is not delegated to you')
+
+ # determine if the entered range overlaps with any AS already
+ # allocated to this child
+ if self.child.asns.filter(end_as__gte=r.min, start_as__lte=r.max).exists():
+ raise forms.ValidationError(
+ 'Overlap with previous allocation to this child')
+
+ return str(r)
+
+
+class AddNetForm(forms.Form):
+ """
+ Returns a forms.Form subclass which validates that the entered address
+ range is within the resources allocated to the parent, and does not overlap
+ with what is already allocated to the specified child.
+
+ """
+ address_range = forms.CharField(
+ help_text='CIDR or range',
+ widget=forms.TextInput(attrs={'autofocus': 'true'})
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child')
+ super(AddNetForm, self).__init__(*args, **kwargs)
+
+ def clean_address_range(self):
+ address_range = self.cleaned_data.get('address_range')
+ try:
+ r = resource_range_ip.parse_str(address_range)
+ if r.version == 6:
+ qs = models.ResourceRangeAddressV6
+ version = 'IPv6'
+ else:
+ qs = models.ResourceRangeAddressV4
+ version = 'IPv4'
+ except BadIPResource:
+ raise forms.ValidationError('invalid IP address range')
+
+ if not qs.objects.filter(cert__conf=self.child.issuer,
+ prefix_min__lte=r.min,
+ prefix_max__gte=r.max).exists():
+ raise forms.ValidationError(
+ 'IP address range is not delegated to you')
+
+ # determine if the entered range overlaps with any prefix
+ # already allocated to this child
+ for n in self.child.address_ranges.filter(version=version):
+ rng = n.as_resource_range()
+ if r.max >= rng.min and r.min <= rng.max:
+ raise forms.ValidationError(
+ 'Overlap with previous allocation to this child')
+
+ return str(r)
+
+
+def ChildForm(instance):
+ """
+ Form for editing a Child model.
+
+ This is roughly based on the equivalent ModelForm, but uses Form as a base
+ class so that selection boxes for the AS and Prefixes can be edited in a
+ single form.
+
+ """
+
+ class _wrapped(forms.Form):
+ valid_until = forms.DateTimeField(initial=instance.valid_until)
+ as_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildASN.objects.filter(child=instance),
+ required=False,
+ label='AS Ranges',
+ help_text='deselect to remove delegation')
+ address_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildNet.objects.filter(child=instance),
+ required=False,
+ help_text='deselect to remove delegation')
+
+ return _wrapped
+
+
+class Empty(forms.Form):
+ """Stub form for views requiring confirmation."""
+ pass
+
+
+class ResourceHolderForm(forms.Form):
+ """form for editing ACL on Conf objects."""
+ users = forms.ModelMultipleChoiceField(
+ queryset=User.objects.all(),
+ help_text='users allowed to mange this resource holder'
+ )
+
+
+class ResourceHolderCreateForm(forms.Form):
+ """form for creating new resource holdres."""
+ handle = forms.CharField(max_length=30)
+ parent = forms.ModelChoiceField(
+ required=False,
+ queryset=models.Conf.objects.all(),
+ help_text='optionally make the new resource holder a child of this resource holder'
+ )
+ users = forms.ModelMultipleChoiceField(
+ required=False,
+ queryset=User.objects.all(),
+ help_text='users allowed to mange this resource holder'
+ )
+
+ def clean_handle(self):
+ handle = self.cleaned_data.get('handle')
+ if models.Conf.objects.filter(handle=handle).exists():
+ raise forms.ValidationError(
+ 'a resource holder with that handle already exists'
+ )
+ return handle
+
+ def clean(self):
+ handle = self.cleaned_data.get('handle')
+ parent = self.cleaned_data.get('parent')
+ if handle and parent and parent.children.filter(handle=handle).exists():
+ raise forms.ValidationError('parent already has a child by that name')
+ return self.cleaned_data
diff --git a/rpki/gui/app/glue.py b/rpki/gui/app/glue.py
new file mode 100644
index 00000000..a9f6441e
--- /dev/null
+++ b/rpki/gui/app/glue.py
@@ -0,0 +1,132 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+"""
+This file contains code that interfaces between the django views implementing
+the portal gui and the rpki.* modules.
+
+"""
+
+from __future__ import with_statement
+
+__version__ = '$Id$'
+
+from datetime import datetime
+
+from rpki.resource_set import (resource_set_as, resource_set_ipv4,
+ resource_set_ipv6, resource_range_ipv4,
+ resource_range_ipv6)
+from rpki.left_right import list_received_resources_elt, report_error_elt
+from rpki.irdb.zookeeper import Zookeeper
+from rpki.gui.app import models
+from rpki.exceptions import BadIPResource
+
+from django.contrib.auth.models import User
+from django.db.transaction import commit_on_success
+
+
+def ghostbuster_to_vcard(gbr):
+ """Convert a GhostbusterRequest object into a vCard object."""
+ import vobject
+
+ vcard = vobject.vCard()
+ vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name,
+ given=gbr.given_name)
+
+ adr_fields = ['box', 'extended', 'street', 'city', 'region', 'code',
+ 'country']
+ adr_dict = dict((f, getattr(gbr, f, '')) for f in adr_fields)
+ if any(adr_dict.itervalues()):
+ vcard.add('ADR').value = vobject.vcard.Address(**adr_dict)
+
+ # mapping from vCard type to Ghostbuster model field
+ # the ORG type is a sequence of organization unit names, so
+ # transform the org name into a tuple before stuffing into the
+ # vCard object
+ attrs = [('FN', 'full_name', None),
+ ('TEL', 'telephone', None),
+ ('ORG', 'organization', lambda x: (x,)),
+ ('EMAIL', 'email_address', None)]
+ for vtype, field, transform in attrs:
+ v = getattr(gbr, field)
+ if v:
+ vcard.add(vtype).value = transform(v) if transform else v
+ return vcard.serialize()
+
+
+class LeftRightError(Exception):
+ """Class for wrapping report_error_elt errors from Zookeeper.call_rpkid().
+
+ It expects a single argument, which is the associated report_error_elt instance."""
+
+ def __str__(self):
+ return 'Error occurred while communicating with rpkid: handle=%s code=%s text=%s' % (
+ self.args[0].self_handle,
+ self.args[0].error_code,
+ self.args[0].error_text)
+
+
+@commit_on_success
+def list_received_resources(log, conf):
+ """
+ Query rpkid for this resource handle's received resources.
+
+ The semantics are to clear the entire table and populate with the list of
+ certs received. Other models should not reference the table directly with
+ foreign keys.
+
+ """
+
+ z = Zookeeper(handle=conf.handle)
+ pdus = z.call_rpkid(list_received_resources_elt.make_pdu(self_handle=conf.handle))
+ # pdus is sometimes None (see https://trac.rpki.net/ticket/681)
+ if pdus is None:
+ print >>log, 'error: call_rpkid() returned None for handle %s when fetching received resources' % conf.handle
+ return
+
+ models.ResourceCert.objects.filter(conf=conf).delete()
+
+ for pdu in pdus:
+ if isinstance(pdu, report_error_elt):
+ # this will cause the db to be rolled back so the above delete()
+ # won't clobber existing resources
+ raise LeftRightError, pdu
+ elif isinstance(pdu, list_received_resources_elt):
+ if pdu.parent_handle != conf.handle:
+ parent = models.Parent.objects.get(issuer=conf,
+ handle=pdu.parent_handle)
+ else:
+ # root cert, self-signed
+ parent = None
+
+ not_before = datetime.strptime(pdu.notBefore, "%Y-%m-%dT%H:%M:%SZ")
+ not_after = datetime.strptime(pdu.notAfter, "%Y-%m-%dT%H:%M:%SZ")
+
+ cert = models.ResourceCert.objects.create(
+ conf=conf, parent=parent, not_before=not_before,
+ not_after=not_after, uri=pdu.uri)
+
+ for asn in resource_set_as(pdu.asn):
+ cert.asn_ranges.create(min=asn.min, max=asn.max)
+
+ for rng in resource_set_ipv4(pdu.ipv4):
+ cert.address_ranges.create(prefix_min=rng.min,
+ prefix_max=rng.max)
+
+ for rng in resource_set_ipv6(pdu.ipv6):
+ cert.address_ranges_v6.create(prefix_min=rng.min,
+ prefix_max=rng.max)
+ else:
+ print >>log, "error: unexpected pdu from rpkid type=%s" % type(pdu)
diff --git a/rpki/gui/app/migrations/0001_initial.py b/rpki/gui/app/migrations/0001_initial.py
new file mode 100644
index 00000000..80877901
--- /dev/null
+++ b/rpki/gui/app/migrations/0001_initial.py
@@ -0,0 +1,192 @@
+# -*- 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 'ResourceCert'
+ db.create_table('app_resourcecert', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('parent', self.gf('django.db.models.fields.related.ForeignKey')(related_name='certs', to=orm['irdb.Parent'])),
+ ('not_before', self.gf('django.db.models.fields.DateTimeField')()),
+ ('not_after', self.gf('django.db.models.fields.DateTimeField')()),
+ ('uri', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('app', ['ResourceCert'])
+
+ # Adding model 'ResourceRangeAddressV4'
+ db.create_table('app_resourcerangeaddressv4', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('prefix_min', self.gf('rpki.gui.models.IPv4AddressField')(db_index=True)),
+ ('prefix_max', self.gf('rpki.gui.models.IPv4AddressField')(db_index=True)),
+ ('cert', self.gf('django.db.models.fields.related.ForeignKey')(related_name='address_ranges', to=orm['app.ResourceCert'])),
+ ))
+ db.send_create_signal('app', ['ResourceRangeAddressV4'])
+
+ # Adding model 'ResourceRangeAddressV6'
+ db.create_table('app_resourcerangeaddressv6', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('prefix_min', self.gf('rpki.gui.models.IPv6AddressField')(db_index=True)),
+ ('prefix_max', self.gf('rpki.gui.models.IPv6AddressField')(db_index=True)),
+ ('cert', self.gf('django.db.models.fields.related.ForeignKey')(related_name='address_ranges_v6', to=orm['app.ResourceCert'])),
+ ))
+ db.send_create_signal('app', ['ResourceRangeAddressV6'])
+
+ # Adding model 'ResourceRangeAS'
+ db.create_table('app_resourcerangeas', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('min', self.gf('django.db.models.fields.PositiveIntegerField')()),
+ ('max', self.gf('django.db.models.fields.PositiveIntegerField')()),
+ ('cert', self.gf('django.db.models.fields.related.ForeignKey')(related_name='asn_ranges', to=orm['app.ResourceCert'])),
+ ))
+ db.send_create_signal('app', ['ResourceRangeAS'])
+
+ # Adding model 'GhostbusterRequest'
+ db.create_table('app_ghostbusterrequest', (
+ ('ghostbusterrequest_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['irdb.GhostbusterRequest'], unique=True, primary_key=True)),
+ ('full_name', self.gf('django.db.models.fields.CharField')(max_length=40)),
+ ('family_name', self.gf('django.db.models.fields.CharField')(max_length=20)),
+ ('given_name', self.gf('django.db.models.fields.CharField')(max_length=20)),
+ ('additional_name', self.gf('django.db.models.fields.CharField')(max_length=20, null=True, blank=True)),
+ ('honorific_prefix', self.gf('django.db.models.fields.CharField')(max_length=10, null=True, blank=True)),
+ ('honorific_suffix', self.gf('django.db.models.fields.CharField')(max_length=10, null=True, blank=True)),
+ ('email_address', self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True)),
+ ('organization', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ('telephone', self.gf('rpki.gui.app.models.TelephoneField')(max_length=40, null=True, blank=True)),
+ ('box', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('extended', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ('street', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+ ('city', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('region', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('code', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('country', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ))
+ db.send_create_signal('app', ['GhostbusterRequest'])
+
+ # Adding model 'Timestamp'
+ db.create_table('app_timestamp', (
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=30, primary_key=True)),
+ ('ts', self.gf('django.db.models.fields.DateTimeField')()),
+ ))
+ db.send_create_signal('app', ['Timestamp'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'ResourceCert'
+ db.delete_table('app_resourcecert')
+
+ # Deleting model 'ResourceRangeAddressV4'
+ db.delete_table('app_resourcerangeaddressv4')
+
+ # Deleting model 'ResourceRangeAddressV6'
+ db.delete_table('app_resourcerangeaddressv6')
+
+ # Deleting model 'ResourceRangeAS'
+ db.delete_table('app_resourcerangeas')
+
+ # Deleting model 'GhostbusterRequest'
+ db.delete_table('app_ghostbusterrequest')
+
+ # Deleting model 'Timestamp'
+ db.delete_table('app_timestamp')
+
+
+ models = {
+ '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'},
+ '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'", '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', [], {})
+ },
+ '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/rpki/gui/app/migrations/0002_auto__add_field_resourcecert_conf.py b/rpki/gui/app/migrations/0002_auto__add_field_resourcecert_conf.py
new file mode 100644
index 00000000..d3326f90
--- /dev/null
+++ b/rpki/gui/app/migrations/0002_auto__add_field_resourcecert_conf.py
@@ -0,0 +1,117 @@
+# -*- 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 field 'ResourceCert.conf'
+ db.add_column('app_resourcecert', 'conf',
+ self.gf('django.db.models.fields.related.ForeignKey')(related_name='certs', null=True, to=orm['irdb.ResourceHolderCA']),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'ResourceCert.conf'
+ db.delete_column('app_resourcecert', 'conf_id')
+
+
+ models = {
+ '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'", 'null': 'True', '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'", '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', [], {})
+ },
+ '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/rpki/gui/app/migrations/0003_set_conf_from_parent.py b/rpki/gui/app/migrations/0003_set_conf_from_parent.py
new file mode 100644
index 00000000..a90a11cc
--- /dev/null
+++ b/rpki/gui/app/migrations/0003_set_conf_from_parent.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
+ for cert in orm.ResourceCert.objects.all():
+ cert.conf = cert.parent.issuer
+ cert.save()
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+ models = {
+ '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'", 'null': 'True', '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'", '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', [], {})
+ },
+ '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']
+ symmetrical = True
diff --git a/rpki/gui/app/migrations/0004_auto__chg_field_resourcecert_conf.py b/rpki/gui/app/migrations/0004_auto__chg_field_resourcecert_conf.py
new file mode 100644
index 00000000..a236ad4a
--- /dev/null
+++ b/rpki/gui/app/migrations/0004_auto__chg_field_resourcecert_conf.py
@@ -0,0 +1,115 @@
+# -*- 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):
+
+ # Changing field 'ResourceCert.conf'
+ db.alter_column('app_resourcecert', 'conf_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['irdb.ResourceHolderCA']))
+
+ def backwards(self, orm):
+
+ # Changing field 'ResourceCert.conf'
+ db.alter_column('app_resourcecert', 'conf_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['irdb.ResourceHolderCA']))
+
+ models = {
+ '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'", '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', [], {})
+ },
+ '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']
diff --git a/rpki/gui/app/migrations/0005_auto__chg_field_resourcecert_parent.py b/rpki/gui/app/migrations/0005_auto__chg_field_resourcecert_parent.py
new file mode 100644
index 00000000..11e9c814
--- /dev/null
+++ b/rpki/gui/app/migrations/0005_auto__chg_field_resourcecert_parent.py
@@ -0,0 +1,115 @@
+# -*- 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):
+
+ # Changing field 'ResourceCert.parent'
+ db.alter_column('app_resourcecert', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['irdb.Parent']))
+
+ def backwards(self, orm):
+
+ # Changing field 'ResourceCert.parent'
+ db.alter_column('app_resourcecert', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['irdb.Parent']))
+
+ models = {
+ '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', [], {})
+ },
+ '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/rpki/gui/app/migrations/0006_add_conf_acl.py b/rpki/gui/app/migrations/0006_add_conf_acl.py
new file mode 100644
index 00000000..88fe8171
--- /dev/null
+++ b/rpki/gui/app/migrations/0006_add_conf_acl.py
@@ -0,0 +1,168 @@
+# -*- 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 'ConfACL'
+ db.create_table('app_confacl', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('conf', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['irdb.ResourceHolderCA'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ))
+ db.send_create_signal('app', ['ConfACL'])
+
+ # Adding unique constraint on 'ConfACL', fields ['user', 'conf']
+ db.create_unique('app_confacl', ['user_id', 'conf_id'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'ConfACL', fields ['user', 'conf']
+ db.delete_unique('app_confacl', ['user_id', 'conf_id'])
+
+ # Deleting model 'ConfACL'
+ db.delete_table('app_confacl')
+
+
+ models = {
+ '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/rpki/gui/app/migrations/0007_default_acls.py b/rpki/gui/app/migrations/0007_default_acls.py
new file mode 100644
index 00000000..40656d0f
--- /dev/null
+++ b/rpki/gui/app/migrations/0007_default_acls.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.core.exceptions import ObjectDoesNotExist
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
+ for conf in orm['irdb.ResourceHolderCA'].objects.all():
+ try:
+ user = orm['auth.User'].objects.get(username=conf.handle)
+ orm['app.ConfACL'].objects.create(
+ conf=conf,
+ user=user
+ )
+ except ObjectDoesNotExist:
+ pass
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ orm['app.ConfACL'].objects.all().delete()
+
+ models = {
+ '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']
+ symmetrical = True
diff --git a/rpki/gui/app/migrations/0008_add_alerts.py b/rpki/gui/app/migrations/0008_add_alerts.py
new file mode 100644
index 00000000..77af68d2
--- /dev/null
+++ b/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/rpki/gui/app/migrations/__init__.py b/rpki/gui/app/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/app/migrations/__init__.py
diff --git a/rpki/gui/app/models.py b/rpki/gui/app/models.py
new file mode 100644
index 00000000..7d643fdc
--- /dev/null
+++ b/rpki/gui/app/models.py
@@ -0,0 +1,420 @@
+# Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__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
+import rpki.irdb.models
+import rpki.gui.models
+import rpki.gui.routeview.models
+from south.modelsinspector import add_introspection_rules
+
+
+class TelephoneField(models.CharField):
+ def __init__(self, **kwargs):
+ if 'max_length' not in kwargs:
+ kwargs['max_length'] = 40
+ models.CharField.__init__(self, **kwargs)
+
+add_introspection_rules([], ['^rpki\.gui\.app\.models\.TelephoneField'])
+
+
+class Parent(rpki.irdb.models.Parent):
+ """proxy model for irdb Parent"""
+
+ def __unicode__(self):
+ return u"%s's parent %s" % (self.issuer.handle, self.handle)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.parent_detail', [str(self.pk)])
+
+ class Meta:
+ proxy = True
+
+
+class Child(rpki.irdb.models.Child):
+ """proxy model for irdb Child"""
+
+ def __unicode__(self):
+ return u"%s's child %s" % (self.issuer.handle, self.handle)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.child_detail', [str(self.pk)])
+
+ class Meta:
+ proxy = True
+ verbose_name_plural = 'children'
+
+
+class ChildASN(rpki.irdb.models.ChildASN):
+ """Proxy model for irdb ChildASN."""
+
+ class Meta:
+ proxy = True
+
+ def __unicode__(self):
+ return u'AS%s' % self.as_resource_range()
+
+
+class ChildNet(rpki.irdb.models.ChildNet):
+ """Proxy model for irdb ChildNet."""
+
+ class Meta:
+ proxy = True
+
+ def __unicode__(self):
+ 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>
+ in the rpkid schema.
+
+ """
+ @property
+ def parents(self):
+ """Simulates irdb.models.Parent.objects, but returns app.models.Parent
+ proxy objects.
+
+ """
+ return Parent.objects.filter(issuer=self)
+
+ @property
+ def children(self):
+ """Simulates irdb.models.Child.objects, but returns app.models.Child
+ proxy objects.
+
+ """
+ return Child.objects.filter(issuer=self)
+
+ @property
+ def ghostbusters(self):
+ return GhostbusterRequest.objects.filter(issuer=self)
+
+ @property
+ def repositories(self):
+ return Repository.objects.filter(issuer=self)
+
+ @property
+ def roas(self):
+ return ROARequest.objects.filter(issuer=self)
+
+ @property
+ def routes(self):
+ """Return all IPv4 routes covered by RPKI certs issued to this resource
+ holder.
+
+ """
+ # build a Q filter to select all RouteOrigin objects covered by
+ # prefixes in the resource holder's certificates
+ q = models.Q()
+ for p in ResourceRangeAddressV4.objects.filter(cert__conf=self):
+ q |= models.Q(prefix_min__gte=p.prefix_min,
+ prefix_max__lte=p.prefix_max)
+ return RouteOrigin.objects.filter(q)
+
+ @property
+ def routes_v6(self):
+ """Return all IPv6 routes covered by RPKI certs issued to this resource
+ holder.
+
+ """
+ # build a Q filter to select all RouteOrigin objects covered by
+ # prefixes in the resource holder's certificates
+ q = models.Q()
+ for p in ResourceRangeAddressV6.objects.filter(cert__conf=self):
+ q |= models.Q(prefix_min__gte=p.prefix_min,
+ 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
+
+ def clear_alerts(self):
+ self.alerts.all().delete()
+
+
+ class Meta:
+ proxy = True
+
+
+class ResourceCert(models.Model):
+ """Represents a resource certificate.
+
+ This model is used to cache the output of <list_received_resources/>.
+
+ """
+
+ # Handle to which this cert was issued
+ conf = models.ForeignKey(Conf, related_name='certs')
+
+ # The parent that issued the cert. This field is marked null=True because
+ # the root has no parent
+ parent = models.ForeignKey(Parent, related_name='certs', null=True)
+
+ # certificate validity period
+ not_before = models.DateTimeField()
+ not_after = models.DateTimeField()
+
+ # Locator for this object. Used to look up the validation status, expiry
+ # of ancestor certs in cacheview
+ uri = models.CharField(max_length=255)
+
+ def __unicode__(self):
+ if self.parent:
+ return u"%s's cert from %s" % (self.conf.handle,
+ self.parent.handle)
+ else:
+ return u"%s's root cert" % self.conf.handle
+
+ def get_cert_chain(self):
+ """Return a list containing the complete certificate chain for this
+ certificate."""
+ cert = self
+ x = [cert]
+ while cert.issuer:
+ cert = cert.issuer
+ x.append(cert)
+ x.reverse()
+ return x
+ cert_chain = property(get_cert_chain)
+
+
+class ResourceRangeAddressV4(rpki.gui.models.PrefixV4):
+ cert = models.ForeignKey(ResourceCert, related_name='address_ranges')
+
+
+class ResourceRangeAddressV6(rpki.gui.models.PrefixV6):
+ cert = models.ForeignKey(ResourceCert, related_name='address_ranges_v6')
+
+
+class ResourceRangeAS(rpki.gui.models.ASN):
+ cert = models.ForeignKey(ResourceCert, related_name='asn_ranges')
+
+
+class ROARequest(rpki.irdb.models.ROARequest):
+ class Meta:
+ proxy = True
+
+ def __unicode__(self):
+ return u"%s's ROA request for AS%d" % (self.issuer.handle, self.asn)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.roa_detail', [str(self.pk)])
+
+ @property
+ def routes(self):
+ "Return all IPv4 routes covered by this roa prefix."
+ # this assumes one prefix per ROA
+ rng = self.prefixes.filter(version=4)[0].as_resource_range()
+ return rpki.gui.routeview.models.RouteOrigin.objects.filter(
+ prefix_min__gte=rng.min,
+ prefix_max__lte=rng.max
+ )
+
+ @property
+ def routes_v6(self):
+ "Return all IPv6 routes covered by this roa prefix."
+ # this assumes one prefix per ROA
+ rng = self.prefixes.filter(version=6)[0].as_resource_range()
+ return rpki.gui.routeview.models.RouteOriginV6.objects.filter(
+ prefix_min__gte=rng.min,
+ prefix_max__lte=rng.max
+ )
+
+
+class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix):
+ class Meta:
+ proxy = True
+
+ def __unicode__(self):
+ return u'ROA Request Prefix %s' % str(self.as_roa_prefix())
+
+
+class GhostbusterRequest(rpki.irdb.models.GhostbusterRequest):
+ """
+ Stores the information require to fill out a vCard entry to
+ populate a ghostbusters record.
+
+ This model is inherited from the irdb GhostBusterRequest model so
+ that the broken out fields can be included for ease of editing.
+ """
+
+ full_name = models.CharField(max_length=40)
+
+ # components of the vCard N type
+ family_name = models.CharField(max_length=20)
+ given_name = models.CharField(max_length=20)
+ additional_name = models.CharField(max_length=20, blank=True, null=True)
+ honorific_prefix = models.CharField(max_length=10, blank=True, null=True)
+ honorific_suffix = models.CharField(max_length=10, blank=True, null=True)
+
+ email_address = models.EmailField(blank=True, null=True)
+ organization = models.CharField(blank=True, null=True, max_length=255)
+ telephone = TelephoneField(blank=True, null=True)
+
+ # elements of the ADR type
+ box = models.CharField(verbose_name='P.O. Box', blank=True, null=True,
+ max_length=40)
+ extended = models.CharField(blank=True, null=True, max_length=255)
+ street = models.CharField(blank=True, null=True, max_length=255)
+ city = models.CharField(blank=True, null=True, max_length=40)
+ region = models.CharField(blank=True, null=True, max_length=40,
+ help_text='state or province')
+ code = models.CharField(verbose_name='Postal Code', blank=True, null=True,
+ max_length=40)
+ country = models.CharField(blank=True, null=True, max_length=40)
+
+ def __unicode__(self):
+ return u"%s's GBR: %s" % (self.issuer.handle, self.full_name)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('gbr-detail', [str(self.pk)])
+
+ class Meta:
+ ordering = ('family_name', 'given_name')
+
+
+class Timestamp(models.Model):
+ """Model to hold metadata about the collection of external data.
+
+ This model is a hash table mapping a timestamp name to the
+ timestamp value. All timestamps values are in UTC.
+
+ The utility function rpki.gui.app.timestmap.update(name) should be used to
+ set timestamps rather than updating this model directly."""
+
+ name = models.CharField(max_length=30, primary_key=True)
+ ts = models.DateTimeField(null=False)
+
+ def __unicode__(self):
+ return '%s: %s' % (self.name, self.ts)
+
+
+class Repository(rpki.irdb.models.Repository):
+ class Meta:
+ proxy = True
+ verbose_name = 'Repository'
+ verbose_name_plural = 'Repositories'
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.repository_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ return "%s's repository %s" % (self.issuer.handle, self.handle)
+
+
+class Client(rpki.irdb.models.Client):
+ "Proxy model for pubd clients."
+
+ class Meta:
+ proxy = True
+ verbose_name = 'Client'
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.client_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ return self.handle
+
+
+class RouteOrigin(rpki.gui.routeview.models.RouteOrigin):
+ class Meta:
+ proxy = True
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.route_detail', [str(self.pk)])
+
+
+class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6):
+ class Meta:
+ proxy = True
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.route_detail', [str(self.pk)])
+
+
+class ConfACL(models.Model):
+ """Stores access control for which users are allowed to manage a given
+ resource handle.
+
+ """
+
+ conf = models.ForeignKey(Conf)
+ user = models.ForeignKey(User)
+
+ class Meta:
+ unique_together = (('user', 'conf'))
diff --git a/rpki/gui/app/range_list.py b/rpki/gui/app/range_list.py
new file mode 100755
index 00000000..21fd1f29
--- /dev/null
+++ b/rpki/gui/app/range_list.py
@@ -0,0 +1,252 @@
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+import bisect
+import unittest
+
+
+class RangeList(list):
+ """A sorted list of ranges, which automatically merges adjacent ranges.
+
+ Items in the list are expected to have ".min" and ".max" attributes."""
+
+ def __init__(self, ini=None):
+ list.__init__(self)
+ if ini:
+ self.extend(ini)
+
+ def append(self, v):
+ keys = [x.min for x in self]
+
+ # lower bound
+ i = bisect.bisect_left(keys, v.min)
+
+ # upper bound
+ j = bisect.bisect_right(keys, v.max, lo=i)
+
+ # if the max value for the previous item is greater than v.min, include
+ # the previous item in the range to replace and use its min value.
+ # also include the previous item if the max value is 1 less than the
+ # min value for the inserted item
+ if i > 0 and self[i - 1].max >= v.min - 1:
+ i = i - 1
+ vmin = self[i].min
+ else:
+ vmin = v.min
+
+ # if the max value for the previous item is greater than the max value
+ # for the new item, use the previous item's max
+ if j > 0 and self[j - 1].max > v.max:
+ vmax = self[j - 1].max
+ else:
+ vmax = v.max
+
+ # if the max value for the new item is 1 less than the min value for
+ # the next item, combine into a single item
+ if j < len(self) and vmax + 1 == self[j].min:
+ vmax = self[j].max
+ j = j + 1
+
+ # replace the range with a new object covering the entire range
+ self[i:j] = [v.__class__(vmin, vmax)]
+
+ def extend(self, args):
+ for x in args:
+ self.append(x)
+
+ def difference(self, other):
+ """Return a RangeList object which contains ranges in this object which
+ are not in "other"."""
+ it = iter(other)
+
+ try:
+ cur = it.next()
+ except StopIteration:
+ return self
+
+ r = RangeList()
+
+ for x in self:
+ xmin = x.min
+
+ def V(v):
+ """convert the integer value to the appropriate type for this
+ range"""
+ return x.__class__.datum_type(v)
+
+ try:
+ while xmin <= x.max:
+ if xmin < cur.min:
+ r.append(x.__class__(V(xmin),
+ V(min(x.max, cur.min - 1))))
+ xmin = cur.max + 1
+ elif xmin == cur.min:
+ xmin = cur.max + 1
+ else: # xmin > cur.min
+ if xmin <= cur.max:
+ xmin = cur.max + 1
+ else: # xmin > cur.max
+ cur = it.next()
+
+ except StopIteration:
+ r.append(x.__class__(V(xmin), x.max))
+
+ return r
+
+
+class TestRangeList(unittest.TestCase):
+ class MinMax(object):
+ datum_type = int
+
+ def __init__(self, range_min, range_max):
+ self.min = range_min
+ self.max = range_max
+
+ def __str__(self):
+ return '(%d, %d)' % (self.min, self.max)
+
+ def __repr__(self):
+ return '<MinMax: (%d, %d)>' % (self.min, self.max)
+
+ def __eq__(self, other):
+ return self.min == other.min and self.max == other.max
+
+ def setUp(self):
+ self.v1 = TestRangeList.MinMax(1, 2)
+ self.v2 = TestRangeList.MinMax(4, 5)
+ self.v3 = TestRangeList.MinMax(7, 8)
+ self.v4 = TestRangeList.MinMax(3, 4)
+ self.v5 = TestRangeList.MinMax(2, 3)
+ self.v6 = TestRangeList.MinMax(1, 10)
+
+ def test_empty_append(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v1)
+
+ def test_no_overlap(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v2)
+ self.assertTrue(len(s) == 2)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+
+ def test_no_overlap_prepend(self):
+ s = RangeList()
+ s.append(self.v2)
+ s.append(self.v1)
+ self.assertTrue(len(s) == 2)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+
+ def test_insert_middle(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v3)
+ s.append(self.v2)
+ self.assertTrue(len(s) == 3)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+ self.assertEqual(s[2], self.v3)
+
+ def test_append_overlap(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v5)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1, 3))
+
+ def test_combine_range(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v4)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1, 4))
+
+ def test_append_subset(self):
+ s = RangeList()
+ s.append(self.v6)
+ s.append(self.v3)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_append_equal(self):
+ s = RangeList()
+ s.append(self.v6)
+ s.append(self.v6)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_prepend_combine(self):
+ s = RangeList()
+ s.append(self.v4)
+ s.append(self.v1)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1, 4))
+
+ def test_append_aggregate(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v2)
+ s.append(self.v3)
+ s.append(self.v6)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_diff_empty(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertEqual(s, s.difference([]))
+
+ def test_diff_self(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertEqual(s.difference(s), [])
+
+ def test_diff_middle(self):
+ s1 = RangeList([self.v6])
+ s2 = RangeList([self.v3])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1, 6), TestRangeList.MinMax(9, 10)]))
+
+ def test_diff_overlap(self):
+ s1 = RangeList([self.v2])
+ s2 = RangeList([self.v4])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(5, 5)]))
+
+ def test_diff_overlap2(self):
+ s1 = RangeList([self.v2])
+ s2 = RangeList([self.v4])
+ self.assertEqual(s2.difference(s1), RangeList([TestRangeList.MinMax(3, 3)]))
+
+ def test_diff_multi(self):
+ s1 = RangeList([TestRangeList.MinMax(1, 2), TestRangeList.MinMax(4, 5)])
+ s2 = RangeList([TestRangeList.MinMax(4, 4)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1, 2), TestRangeList.MinMax(5, 5)]))
+
+ def test_diff_multi_overlap(self):
+ s1 = RangeList([TestRangeList.MinMax(1, 2), TestRangeList.MinMax(3, 4)])
+ s2 = RangeList([TestRangeList.MinMax(2, 3)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4)]))
+
+ def test_diff_multi_overlap2(self):
+ s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4), TestRangeList.MinMax(6,7)])
+ s2 = RangeList([TestRangeList.MinMax(2, 3), TestRangeList.MinMax(6, 6)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4), TestRangeList.MinMax(7,7)]))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/rpki/gui/app/static/css/bootstrap.min.css b/rpki/gui/app/static/css/bootstrap.min.css
new file mode 100644
index 00000000..c10c7f41
--- /dev/null
+++ b/rpki/gui/app/static/css/bootstrap.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Bootstrap v2.3.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed}
diff --git a/rpki/gui/app/static/img/glyphicons-halflings-white.png b/rpki/gui/app/static/img/glyphicons-halflings-white.png
new file mode 100644
index 00000000..3bf6484a
--- /dev/null
+++ b/rpki/gui/app/static/img/glyphicons-halflings-white.png
Binary files differ
diff --git a/rpki/gui/app/static/img/glyphicons-halflings.png b/rpki/gui/app/static/img/glyphicons-halflings.png
new file mode 100644
index 00000000..a9969993
--- /dev/null
+++ b/rpki/gui/app/static/img/glyphicons-halflings.png
Binary files differ
diff --git a/rpki/gui/app/static/img/sui-riu.ico b/rpki/gui/app/static/img/sui-riu.ico
new file mode 100644
index 00000000..61223e27
--- /dev/null
+++ b/rpki/gui/app/static/img/sui-riu.ico
Binary files differ
diff --git a/rpki/gui/app/static/js/bootstrap.min.js b/rpki/gui/app/static/js/bootstrap.min.js
new file mode 100644
index 00000000..95c5ac5e
--- /dev/null
+++ b/rpki/gui/app/static/js/bootstrap.min.js
@@ -0,0 +1,6 @@
+/*!
+* Bootstrap.js by @fat & @mdo
+* Copyright 2012 Twitter, Inc.
+* http://www.apache.org/licenses/LICENSE-2.0.txt
+*/
+!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f<s.length-1&&f++,~f||(f=0),s.eq(f).focus()}};var s=e.fn.dropdown;e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e.fn.dropdown.noConflict=function(){return e.fn.dropdown=s,this},e(document).on("click.dropdown.data-api",r).on("click.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.focus().trigger("shown")}):t.$element.focus().trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(){var e=this;this.$element.hide(),this.backdrop(function(){e.removeBackdrop(),e.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); \ No newline at end of file
diff --git a/rpki/gui/app/static/js/jquery-1.8.3.min.js b/rpki/gui/app/static/js/jquery-1.8.3.min.js
new file mode 100644
index 00000000..83589daa
--- /dev/null
+++ b/rpki/gui/app/static/js/jquery-1.8.3.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v1.8.3 jquery.com | jquery.org/license */
+(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r<i;r++)v.event.add(t,n,u[n][r])}o.data&&(o.data=v.extend({},o.data))}function Ot(e,t){var n;if(t.nodeType!==1)return;t.clearAttributes&&t.clearAttributes(),t.mergeAttributes&&t.mergeAttributes(e),n=t.nodeName.toLowerCase(),n==="object"?(t.parentNode&&(t.outerHTML=e.outerHTML),v.support.html5Clone&&e.innerHTML&&!v.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):n==="input"&&Et.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):n==="option"?t.selected=e.defaultSelected:n==="input"||n==="textarea"?t.defaultValue=e.defaultValue:n==="script"&&t.text!==e.text&&(t.text=e.text),t.removeAttribute(v.expando)}function Mt(e){return typeof e.getElementsByTagName!="undefined"?e.getElementsByTagName("*"):typeof e.querySelectorAll!="undefined"?e.querySelectorAll("*"):[]}function _t(e){Et.test(e.type)&&(e.defaultChecked=e.checked)}function Qt(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Jt.length;while(i--){t=Jt[i]+n;if(t in e)return t}return r}function Gt(e,t){return e=t||e,v.css(e,"display")==="none"||!v.contains(e.ownerDocument,e)}function Yt(e,t){var n,r,i=[],s=0,o=e.length;for(;s<o;s++){n=e[s];if(!n.style)continue;i[s]=v._data(n,"olddisplay"),t?(!i[s]&&n.style.display==="none"&&(n.style.display=""),n.style.display===""&&Gt(n)&&(i[s]=v._data(n,"olddisplay",nn(n.nodeName)))):(r=Dt(n,"display"),!i[s]&&r!=="none"&&v._data(n,"olddisplay",r))}for(s=0;s<o;s++){n=e[s];if(!n.style)continue;if(!t||n.style.display==="none"||n.style.display==="")n.style.display=t?i[s]||"":"none"}return e}function Zt(e,t,n){var r=Rt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function en(e,t,n,r){var i=n===(r?"border":"content")?4:t==="width"?1:0,s=0;for(;i<4;i+=2)n==="margin"&&(s+=v.css(e,n+$t[i],!0)),r?(n==="content"&&(s-=parseFloat(Dt(e,"padding"+$t[i]))||0),n!=="margin"&&(s-=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0)):(s+=parseFloat(Dt(e,"padding"+$t[i]))||0,n!=="padding"&&(s+=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0));return s}function tn(e,t,n){var r=t==="width"?e.offsetWidth:e.offsetHeight,i=!0,s=v.support.boxSizing&&v.css(e,"boxSizing")==="border-box";if(r<=0||r==null){r=Dt(e,t);if(r<0||r==null)r=e.style[t];if(Ut.test(r))return r;i=s&&(v.support.boxSizingReliable||r===e.style[t]),r=parseFloat(r)||0}return r+en(e,t,n||(s?"border":"content"),i)+"px"}function nn(e){if(Wt[e])return Wt[e];var t=v("<"+e+">").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write("<!doctype html><html><body>"),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u<a;u++)r=o[u],s=/^\+/.test(r),s&&(r=r.substr(1)||"*"),i=e[r]=e[r]||[],i[s?"unshift":"push"](n)}}function kn(e,n,r,i,s,o){s=s||n.dataTypes[0],o=o||{},o[s]=!0;var u,a=e[s],f=0,l=a?a.length:0,c=e===Sn;for(;f<l&&(c||!u);f++)u=a[f](n,r,i),typeof u=="string"&&(!c||o[u]?u=t:(n.dataTypes.unshift(u),u=kn(e,n,r,i,u,o)));return(c||!u)&&!o["*"]&&(u=kn(e,n,r,i,"*",o)),u}function Ln(e,n){var r,i,s=v.ajaxSettings.flatOptions||{};for(r in n)n[r]!==t&&((s[r]?e:i||(i={}))[r]=n[r]);i&&v.extend(!0,e,i)}function An(e,n,r){var i,s,o,u,a=e.contents,f=e.dataTypes,l=e.responseFields;for(s in l)s in r&&(n[l[s]]=r[s]);while(f[0]==="*")f.shift(),i===t&&(i=e.mimeType||n.getResponseHeader("content-type"));if(i)for(s in a)if(a[s]&&a[s].test(i)){f.unshift(s);break}if(f[0]in r)o=f[0];else{for(s in r){if(!f[0]||e.converters[s+" "+f[0]]){o=s;break}u||(u=s)}o=o||u}if(o)return o!==f[0]&&f.unshift(o),r[o]}function On(e,t){var n,r,i,s,o=e.dataTypes.slice(),u=o[0],a={},f=0;e.dataFilter&&(t=e.dataFilter(t,e.dataType));if(o[1])for(n in e.converters)a[n.toLowerCase()]=e.converters[n];for(;i=o[++f];)if(i!=="*"){if(u!=="*"&&u!==i){n=a[u+" "+i]||a["* "+i];if(!n)for(r in a){s=r.split(" ");if(s[1]===i){n=a[u+" "+s[0]]||a["* "+s[0]];if(n){n===!0?n=a[r]:a[r]!==!0&&(i=s[0],o.splice(f--,0,i));break}}}if(n!==!0)if(n&&e["throws"])t=n(t);else try{t=n(t)}catch(l){return{state:"parsererror",error:n?l:"No conversion from "+u+" to "+i}}}u=i}return{state:"success",data:t}}function Fn(){try{return new e.XMLHttpRequest}catch(t){}}function In(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function $n(){return setTimeout(function(){qn=t},0),qn=v.now()}function Jn(e,t){v.each(t,function(t,n){var r=(Vn[t]||[]).concat(Vn["*"]),i=0,s=r.length;for(;i<s;i++)if(r[i].call(e,t,n))return})}function Kn(e,t,n){var r,i=0,s=0,o=Xn.length,u=v.Deferred().always(function(){delete a.elem}),a=function(){var t=qn||$n(),n=Math.max(0,f.startTime+f.duration-t),r=n/f.duration||0,i=1-r,s=0,o=f.tweens.length;for(;s<o;s++)f.tweens[s].run(i);return u.notifyWith(e,[f,i,n]),i<1&&o?n:(u.resolveWith(e,[f]),!1)},f=u.promise({elem:e,props:v.extend({},t),opts:v.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:qn||$n(),duration:n.duration,tweens:[],createTween:function(t,n,r){var i=v.Tween(e,f.opts,t,n,f.opts.specialEasing[t]||f.opts.easing);return f.tweens.push(i),i},stop:function(t){var n=0,r=t?f.tweens.length:0;for(;n<r;n++)f.tweens[n].run(1);return t?u.resolveWith(e,[f,t]):u.rejectWith(e,[f,t]),this}}),l=f.props;Qn(l,f.opts.specialEasing);for(;i<o;i++){r=Xn[i].call(f,e,l,f.opts);if(r)return r}return Jn(f,l),v.isFunction(f.opts.start)&&f.opts.start.call(e,f),v.fx.timer(v.extend(a,{anim:f,queue:f.opts.queue,elem:e})),f.progress(f.opts.progress).done(f.opts.done,f.opts.complete).fail(f.opts.fail).always(f.opts.always)}function Qn(e,t){var n,r,i,s,o;for(n in e){r=v.camelCase(n),i=t[r],s=e[n],v.isArray(s)&&(i=s[1],s=e[n]=s[0]),n!==r&&(e[r]=s,delete e[n]),o=v.cssHooks[r];if(o&&"expand"in o){s=o.expand(s),delete e[r];for(n in s)n in e||(e[n]=s[n],t[n]=i)}else t[r]=i}}function Gn(e,t,n){var r,i,s,o,u,a,f,l,c,h=this,p=e.style,d={},m=[],g=e.nodeType&&Gt(e);n.queue||(l=v._queueHooks(e,"fx"),l.unqueued==null&&(l.unqueued=0,c=l.empty.fire,l.empty.fire=function(){l.unqueued||c()}),l.unqueued++,h.always(function(){h.always(function(){l.unqueued--,v.queue(e,"fx").length||l.empty.fire()})})),e.nodeType===1&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],v.css(e,"display")==="inline"&&v.css(e,"float")==="none"&&(!v.support.inlineBlockNeedsLayout||nn(e.nodeName)==="inline"?p.display="inline-block":p.zoom=1)),n.overflow&&(p.overflow="hidden",v.support.shrinkWrapBlocks||h.done(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t){s=t[r];if(Un.exec(s)){delete t[r],a=a||s==="toggle";if(s===(g?"hide":"show"))continue;m.push(r)}}o=m.length;if(o){u=v._data(e,"fxshow")||v._data(e,"fxshow",{}),"hidden"in u&&(g=u.hidden),a&&(u.hidden=!g),g?v(e).show():h.done(function(){v(e).hide()}),h.done(function(){var t;v.removeData(e,"fxshow",!0);for(t in d)v.style(e,t,d[t])});for(r=0;r<o;r++)i=m[r],f=h.createTween(i,g?u[i]:0),d[i]=u[i]||v.style(e,i),i in u||(u[i]=f.start,g&&(f.end=f.start,f.start=i==="width"||i==="height"?1:0))}}function Yn(e,t,n,r,i){return new Yn.prototype.init(e,t,n,r,i)}function Zn(e,t){var n,r={height:e},i=0;t=t?1:0;for(;i<4;i+=2-t)n=$t[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function tr(e){return v.isWindow(e)?e:e.nodeType===9?e.defaultView||e.parentWindow:!1}var n,r,i=e.document,s=e.location,o=e.navigator,u=e.jQuery,a=e.$,f=Array.prototype.push,l=Array.prototype.slice,c=Array.prototype.indexOf,h=Object.prototype.toString,p=Object.prototype.hasOwnProperty,d=String.prototype.trim,v=function(e,t){return new v.fn.init(e,t,n)},m=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,g=/\S/,y=/\s+/,b=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,w=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a<f;a++)if((e=arguments[a])!=null)for(n in e){r=u[n],i=e[n];if(u===i)continue;l&&i&&(v.isPlainObject(i)||(s=v.isArray(i)))?(s?(s=!1,o=r&&v.isArray(r)?r:[]):o=r&&v.isPlainObject(r)?r:{},u[n]=v.extend(l,o,i)):i!==t&&(u[n]=i)}return u},v.extend({noConflict:function(t){return e.$===v&&(e.$=a),t&&e.jQuery===v&&(e.jQuery=u),v},isReady:!1,readyWait:1,holdReady:function(e){e?v.readyWait++:v.ready(!0)},ready:function(e){if(e===!0?--v.readyWait:v.isReady)return;if(!i.body)return setTimeout(v.ready,1);v.isReady=!0;if(e!==!0&&--v.readyWait>0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s<o;)if(n.apply(e[s++],r)===!1)break}else if(u){for(i in e)if(n.call(e[i],i,e[i])===!1)break}else for(;s<o;)if(n.call(e[s],s,e[s++])===!1)break;return e},trim:d&&!d.call("\ufeff\u00a0")?function(e){return e==null?"":d.call(e)}:function(e){return e==null?"":(e+"").replace(b,"")},makeArray:function(e,t){var n,r=t||[];return e!=null&&(n=v.type(e),e.length==null||n==="string"||n==="function"||n==="regexp"||v.isWindow(e)?f.call(r,e):v.merge(r,e)),r},inArray:function(e,t,n){var r;if(t){if(c)return c.call(t,e,n);r=t.length,n=n?n<0?Math.max(0,r+n):n:0;for(;n<r;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,s=0;if(typeof r=="number")for(;s<r;s++)e[i++]=n[s];else while(n[s]!==t)e[i++]=n[s++];return e.length=i,e},grep:function(e,t,n){var r,i=[],s=0,o=e.length;n=!!n;for(;s<o;s++)r=!!t(e[s],s),n!==r&&i.push(e[s]);return i},map:function(e,n,r){var i,s,o=[],u=0,a=e.length,f=e instanceof v||a!==t&&typeof a=="number"&&(a>0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u<a;u++)i=n(e[u],u,r),i!=null&&(o[o.length]=i);else for(s in e)i=n(e[s],s,r),i!=null&&(o[o.length]=i);return o.concat.apply([],o)},guid:1,proxy:function(e,n){var r,i,s;return typeof n=="string"&&(r=e[n],n=e,e=r),v.isFunction(e)?(i=l.call(arguments,2),s=function(){return e.apply(n,i.concat(l.call(arguments)))},s.guid=e.guid=e.guid||v.guid++,s):t},access:function(e,n,r,i,s,o,u){var a,f=r==null,l=0,c=e.length;if(r&&typeof r=="object"){for(l in r)v.access(e,n,l,r[l],1,o,i);s=1}else if(i!==t){a=u===t&&v.isFunction(i),f&&(a?(a=n,n=function(e,t,n){return a.call(v(e),n)}):(n.call(e,i),n=null));if(n)for(;l<c;l++)n(e[l],r,a?i.call(e[l],l,n(e[l],r)):i,u);s=1}return s?e:f?n.call(e):c?n(e[0],r):o},now:function(){return(new Date).getTime()}}),v.ready.promise=function(t){if(!r){r=v.Deferred();if(i.readyState==="complete")setTimeout(v.ready,1);else if(i.addEventListener)i.addEventListener("DOMContentLoaded",A,!1),e.addEventListener("load",v.ready,!1);else{i.attachEvent("onreadystatechange",A),e.attachEvent("onload",v.ready);var n=!1;try{n=e.frameElement==null&&i.documentElement}catch(s){}n&&n.doScroll&&function o(){if(!v.isReady){try{n.doScroll("left")}catch(e){return setTimeout(o,50)}v.ready()}}()}}return r.promise(t)},v.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(e,t){O["[object "+t+"]"]=t.toLowerCase()}),n=v(i);var M={};v.Callbacks=function(e){e=typeof e=="string"?M[e]||_(e):v.extend({},e);var n,r,i,s,o,u,a=[],f=!e.once&&[],l=function(t){n=e.memory&&t,r=!0,u=s||0,s=0,o=a.length,i=!0;for(;a&&u<o;u++)if(a[u].apply(t[0],t[1])===!1&&e.stopOnFalse){n=!1;break}i=!1,a&&(f?f.length&&l(f.shift()):n?a=[]:c.disable())},c={add:function(){if(a){var t=a.length;(function r(t){v.each(t,function(t,n){var i=v.type(n);i==="function"?(!e.unique||!c.has(n))&&a.push(n):n&&n.length&&i!=="string"&&r(n)})})(arguments),i?o=a.length:n&&(s=t,l(n))}return this},remove:function(){return a&&v.each(arguments,function(e,t){var n;while((n=v.inArray(t,a,n))>-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t<r;t++)n[t]&&v.isFunction(n[t].promise)?n[t].promise().done(o(t,f,n)).fail(s.reject).progress(o(t,a,u)):--i}return i||s.resolveWith(f,n),s.promise()}}),v.support=function(){var t,n,r,s,o,u,a,f,l,c,h,p=i.createElement("div");p.setAttribute("className","t"),p.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="<table><tr><td></td><td>t</td></tr></table>",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="<div></div>",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i<s;i++)delete r[t[i]];if(!(n?B:v.isEmptyObject)(r))return}}if(!n){delete u[a].data;if(!B(u[a]))return}o?v.cleanData([e],!0):v.support.deleteExpando||u!=u.window?delete u[a]:u[a]=null},_data:function(e,t,n){return v.data(e,t,n,!0)},acceptData:function(e){var t=e.nodeName&&v.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),v.fn.extend({data:function(e,n){var r,i,s,o,u,a=this[0],f=0,l=null;if(e===t){if(this.length){l=v.data(a);if(a.nodeType===1&&!v._data(a,"parsedAttrs")){s=a.attributes;for(u=s.length;f<u;f++)o=s[f].name,o.indexOf("data-")||(o=v.camelCase(o.substring(5)),H(a,o,l[o]));v._data(a,"parsedAttrs",!0)}}return l}return typeof e=="object"?this.each(function(){v.data(this,e)}):(r=e.split(".",2),r[1]=r[1]?"."+r[1]:"",i=r[1]+"!",v.access(this,function(n){if(n===t)return l=this.triggerHandler("getData"+i,[r[0]]),l===t&&a&&(l=v.data(a,e),l=H(a,e,l)),l===t&&r[1]?this.data(r[0]):l;r[1]=n,this.each(function(){var t=v(this);t.triggerHandler("setData"+i,r),v.data(this,e,n),t.triggerHandler("changeData"+i,r)})},null,n,arguments.length>1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length<r?v.queue(this[0],e):n===t?this:this.each(function(){var t=v.queue(this,e,n);v._queueHooks(this,e),e==="fx"&&t[0]!=="inprogress"&&v.dequeue(this,e)})},dequeue:function(e){return this.each(function(){v.dequeue(this,e)})},delay:function(e,t){return e=v.fx?v.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,s=v.Deferred(),o=this,u=this.length,a=function(){--i||s.resolveWith(o,[o])};typeof e!="string"&&(n=e,e=t),e=e||"fx";while(u--)r=v._data(o[u],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(a));return a(),s.promise(n)}});var j,F,I,q=/[\t\r\n]/g,R=/\r/g,U=/^(?:button|input)$/i,z=/^(?:button|input|object|select|textarea)$/i,W=/^a(?:rea|)$/i,X=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,V=v.support.getSetAttribute;v.fn.extend({attr:function(e,t){return v.access(this,v.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n<r;n++){i=this[n];if(i.nodeType===1)if(!i.className&&t.length===1)i.className=e;else{s=" "+i.className+" ";for(o=0,u=t.length;o<u;o++)s.indexOf(" "+t[o]+" ")<0&&(s+=t[o]+" ");i.className=v.trim(s)}}}return this},removeClass:function(e){var n,r,i,s,o,u,a;if(v.isFunction(e))return this.each(function(t){v(this).removeClass(e.call(this,t,this.className))});if(e&&typeof e=="string"||e===t){n=(e||"").split(y);for(u=0,a=this.length;u<a;u++){i=this[u];if(i.nodeType===1&&i.className){r=(" "+i.className+" ").replace(q," ");for(s=0,o=n.length;s<o;s++)while(r.indexOf(" "+n[s]+" ")>=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n<r;n++)if(this[n].nodeType===1&&(" "+this[n].className+" ").replace(q," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a<u;a++){n=r[a];if((n.selected||a===i)&&(v.support.optDisabled?!n.disabled:n.getAttribute("disabled")===null)&&(!n.parentNode.disabled||!v.nodeName(n.parentNode,"optgroup"))){t=v(n).val();if(s)return t;o.push(t)}}return o},set:function(e,t){var n=v.makeArray(t);return v(e).find("option").each(function(){this.selected=v.inArray(v(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o<r.length;o++)i=r[o],i&&(n=v.propFix[i]||i,s=X.test(i),s||v.attr(e,i,""),e.removeAttribute(V?i:n),s&&n in e&&(e[n]=!1))}},attrHooks:{type:{set:function(e,t){if(U.test(e.nodeName)&&e.parentNode)v.error("type property can't be changed");else if(!v.support.radioValue&&t==="radio"&&v.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}},value:{get:function(e,t){return j&&v.nodeName(e,"button")?j.get(e,t):t in e?e.value:null},set:function(e,t,n){if(j&&v.nodeName(e,"button"))return j.set(e,t,n);e.value=t}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,s,o,u=e.nodeType;if(!e||u===3||u===8||u===2)return;return o=u!==1||!v.isXMLDoc(e),o&&(n=v.propFix[n]||n,s=v.propHooks[n]),r!==t?s&&"set"in s&&(i=s.set(e,r,n))!==t?i:e[n]=r:s&&"get"in s&&(i=s.get(e,n))!==null?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):z.test(e.nodeName)||W.test(e.nodeName)&&e.href?0:t}}}}),F={get:function(e,n){var r,i=v.prop(e,n);return i===!0||typeof i!="boolean"&&(r=e.getAttributeNode(n))&&r.nodeValue!==!1?n.toLowerCase():t},set:function(e,t,n){var r;return t===!1?v.removeAttr(e,n):(r=v.propFix[n]||n,r in e&&(e[r]=!0),e.setAttribute(n,n.toLowerCase())),n}},V||(I={name:!0,id:!0,coords:!0},j=v.valHooks.button={get:function(e,n){var r;return r=e.getAttributeNode(n),r&&(I[n]?r.value!=="":r.specified)?r.value:t},set:function(e,t,n){var r=e.getAttributeNode(n);return r||(r=i.createAttribute(n),e.setAttributeNode(r)),r.value=t+""}},v.each(["width","height"],function(e,t){v.attrHooks[t]=v.extend(v.attrHooks[t],{set:function(e,n){if(n==="")return e.setAttribute(t,"auto"),n}})}),v.attrHooks.contenteditable={get:j.get,set:function(e,t,n){t===""&&(t="false"),j.set(e,t,n)}}),v.support.hrefNormalized||v.each(["href","src","width","height"],function(e,n){v.attrHooks[n]=v.extend(v.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return r===null?t:r}})}),v.support.style||(v.attrHooks.style={get:function(e){return e.style.cssText.toLowerCase()||t},set:function(e,t){return e.style.cssText=t+""}}),v.support.optSelected||(v.propHooks.selected=v.extend(v.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),v.support.enctype||(v.propFix.enctype="encoding"),v.support.checkOn||v.each(["radio","checkbox"],function(){v.valHooks[this]={get:function(e){return e.getAttribute("value")===null?"on":e.value}}}),v.each(["radio","checkbox"],function(){v.valHooks[this]=v.extend(v.valHooks[this],{set:function(e,t){if(v.isArray(t))return e.checked=v.inArray(v(e).val(),t)>=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f<n.length;f++){l=J.exec(n[f])||[],c=l[1],h=(l[2]||"").split(".").sort(),g=v.event.special[c]||{},c=(s?g.delegateType:g.bindType)||c,g=v.event.special[c]||{},p=v.extend({type:c,origType:l[1],data:i,handler:r,guid:r.guid,selector:s,needsContext:s&&v.expr.match.needsContext.test(s),namespace:h.join(".")},d),m=a[c];if(!m){m=a[c]=[],m.delegateCount=0;if(!g.setup||g.setup.call(e,i,h,u)===!1)e.addEventListener?e.addEventListener(c,u,!1):e.attachEvent&&e.attachEvent("on"+c,u)}g.add&&(g.add.call(e,p),p.handler.guid||(p.handler.guid=r.guid)),s?m.splice(m.delegateCount++,0,p):m.push(p),v.event.global[c]=!0}e=null},global:{},remove:function(e,t,n,r,i){var s,o,u,a,f,l,c,h,p,d,m,g=v.hasData(e)&&v._data(e);if(!g||!(h=g.events))return;t=v.trim(Z(t||"")).split(" ");for(s=0;s<t.length;s++){o=J.exec(t[s])||[],u=a=o[1],f=o[2];if(!u){for(u in h)v.event.remove(e,u+t[s],n,r,!0);continue}p=v.event.special[u]||{},u=(r?p.delegateType:p.bindType)||u,d=h[u]||[],l=d.length,f=f?new RegExp("(^|\\.)"+f.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(c=0;c<d.length;c++)m=d[c],(i||a===m.origType)&&(!n||n.guid===m.guid)&&(!f||f.test(m.namespace))&&(!r||r===m.selector||r==="**"&&m.selector)&&(d.splice(c--,1),m.selector&&d.delegateCount--,p.remove&&p.remove.call(e,m));d.length===0&&l!==d.length&&((!p.teardown||p.teardown.call(e,f,g.handle)===!1)&&v.removeEvent(e,u,g.handle),delete h[u])}v.isEmptyObject(h)&&(delete g.handle,v.removeData(e,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(n,r,s,o){if(!s||s.nodeType!==3&&s.nodeType!==8){var u,a,f,l,c,h,p,d,m,g,y=n.type||n,b=[];if(Y.test(y+v.event.triggered))return;y.indexOf("!")>=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f<m.length&&!n.isPropagationStopped();f++)l=m[f][0],n.type=m[f][1],d=(v._data(l,"events")||{})[n.type]&&v._data(l,"handle"),d&&d.apply(l,r),d=h&&l[h],d&&v.acceptData(l)&&d.apply&&d.apply(l,r)===!1&&n.preventDefault();return n.type=y,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(s.ownerDocument,r)===!1)&&(y!=="click"||!v.nodeName(s,"a"))&&v.acceptData(s)&&h&&s[y]&&(y!=="focus"&&y!=="blur"||n.target.offsetWidth!==0)&&!v.isWindow(s)&&(c=s[h],c&&(s[h]=null),v.event.triggered=y,s[y](),v.event.triggered=t,c&&(s[h]=c)),n.result}return},dispatch:function(n){n=v.event.fix(n||e.event);var r,i,s,o,u,a,f,c,h,p,d=(v._data(this,"events")||{})[n.type]||[],m=d.delegateCount,g=l.call(arguments),y=!n.exclusive&&!n.namespace,b=v.event.special[n.type]||{},w=[];g[0]=n,n.delegateTarget=this;if(b.preDispatch&&b.preDispatch.call(this,n)===!1)return;if(m&&(!n.button||n.type!=="click"))for(s=n.target;s!=this;s=s.parentNode||this)if(s.disabled!==!0||n.type!=="click"){u={},f=[];for(r=0;r<m;r++)c=d[r],h=c.selector,u[h]===t&&(u[h]=c.needsContext?v(h,this).index(s)>=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r<w.length&&!n.isPropagationStopped();r++){a=w[r],n.currentTarget=a.elem;for(i=0;i<a.matches.length&&!n.isImmediatePropagationStopped();i++){c=a.matches[i];if(y||!n.namespace&&!c.namespace||n.namespace_re&&n.namespace_re.test(c.namespace))n.data=c.data,n.handleObj=c,o=((v.event.special[c.origType]||{}).handle||c.handler).apply(a.elem,g),o!==t&&(n.result=o,o===!1&&(n.preventDefault(),n.stopPropagation()))}}return b.postDispatch&&b.postDispatch.call(this,n),n.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return e.which==null&&(e.which=t.charCode!=null?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,s,o,u=n.button,a=n.fromElement;return e.pageX==null&&n.clientX!=null&&(r=e.target.ownerDocument||i,s=r.documentElement,o=r.body,e.pageX=n.clientX+(s&&s.scrollLeft||o&&o.scrollLeft||0)-(s&&s.clientLeft||o&&o.clientLeft||0),e.pageY=n.clientY+(s&&s.scrollTop||o&&o.scrollTop||0)-(s&&s.clientTop||o&&o.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?n.toElement:a),!e.which&&u!==t&&(e.which=u&1?1:u&2?3:u&4?2:0),e}},fix:function(e){if(e[v.expando])return e;var t,n,r=e,s=v.event.fixHooks[e.type]||{},o=s.props?this.props.concat(s.props):this.props;e=v.Event(r);for(t=o.length;t;)n=o[--t],e[n]=r[n];return e.target||(e.target=r.srcElement||i),e.target.nodeType===3&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,r):e},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(e,t,n){v.isWindow(this)&&(this.onbeforeunload=n)},teardown:function(e,t){this.onbeforeunload===t&&(this.onbeforeunload=null)}}},simulate:function(e,t,n,r){var i=v.extend(new v.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?v.event.trigger(i,null,t):v.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},v.event.handle=v.event.dispatch,v.removeEvent=i.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]=="undefined"&&(e[r]=null),e.detachEvent(r,n))},v.Event=function(e,t){if(!(this instanceof v.Event))return new v.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?tt:et):this.type=e,t&&v.extend(this,t),this.timeStamp=e&&e.timeStamp||v.now(),this[v.expando]=!0},v.Event.prototype={preventDefault:function(){this.isDefaultPrevented=tt;var e=this.originalEvent;if(!e)return;e.preventDefault?e.preventDefault():e.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=tt;var e=this.originalEvent;if(!e)return;e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=tt,this.stopPropagation()},isDefaultPrevented:et,isPropagationStopped:et,isImmediatePropagationStopped:et},v.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){v.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,s=e.handleObj,o=s.selector;if(!i||i!==r&&!v.contains(r,i))e.type=s.origType,n=s.handler.apply(this,arguments),e.type=t;return n}}}),v.support.submitBubbles||(v.event.special.submit={setup:function(){if(v.nodeName(this,"form"))return!1;v.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=v.nodeName(n,"input")||v.nodeName(n,"button")?n.form:t;r&&!v._data(r,"_submit_attached")&&(v.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),v._data(r,"_submit_attached",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&v.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){if(v.nodeName(this,"form"))return!1;v.event.remove(this,"._submit")}}),v.support.changeBubbles||(v.event.special.change={setup:function(){if($.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")v.event.add(this,"propertychange._change",function(e){e.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),v.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),v.event.simulate("change",this,e,!0)});return!1}v.event.add(this,"beforeactivate._change",function(e){var t=e.target;$.test(t.nodeName)&&!v._data(t,"_change_attached")&&(v.event.add(t,"change._change",function(e){this.parentNode&&!e.isSimulated&&!e.isTrigger&&v.event.simulate("change",this.parentNode,e,!0)}),v._data(t,"_change_attached",!0))})},handle:function(e){var t=e.target;if(this!==t||e.isSimulated||e.isTrigger||t.type!=="radio"&&t.type!=="checkbox")return e.handleObj.handler.apply(this,arguments)},teardown:function(){return v.event.remove(this,"._change"),!$.test(this.nodeName)}}),v.support.focusinBubbles||v.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){v.event.simulate(t,e.target,v.event.fix(e),!0)};v.event.special[t]={setup:function(){n++===0&&i.addEventListener(e,r,!0)},teardown:function(){--n===0&&i.removeEventListener(e,r,!0)}}}),v.fn.extend({on:function(e,n,r,i,s){var o,u;if(typeof e=="object"){typeof n!="string"&&(r=r||n,n=t);for(u in e)this.on(u,n,r,e[u],s);return this}r==null&&i==null?(i=n,r=n=t):i==null&&(typeof n=="string"?(i=r,r=t):(i=r,r=n,n=t));if(i===!1)i=et;else if(!i)return this;return s===1&&(o=i,i=function(e){return v().off(e),o.apply(this,arguments)},i.guid=o.guid||(o.guid=v.guid++)),this.each(function(){v.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,s;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,v(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if(typeof e=="object"){for(s in e)this.off(s,n,e[s]);return this}if(n===!1||typeof n=="function")r=n,n=t;return r===!1&&(r=et),this.each(function(){v.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},live:function(e,t,n){return v(this.context).on(e,this.selector,t,n),this},die:function(e,t){return v(this.context).off(e,this.selector||"**",t),this},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return arguments.length===1?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){v.event.trigger(e,t,this)})},triggerHandler:function(e,t){if(this[0])return v.event.trigger(e,t,this[0],!0)},toggle:function(e){var t=arguments,n=e.guid||v.guid++,r=0,i=function(n){var i=(v._data(this,"lastToggle"+e.guid)||0)%r;return v._data(this,"lastToggle"+e.guid,i+1),n.preventDefault(),t[i].apply(this,arguments)||!1};i.guid=n;while(r<t.length)t[r++].guid=n;return this.click(i)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),v.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){v.fn[t]=function(e,n){return n==null&&(n=e,e=null),arguments.length>0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u<a;u++)if(s=e[u])if(!n||n(s,r,i))o.push(s),f&&t.push(u);return o}function ct(e,t,n,r,i,s){return r&&!r[d]&&(r=ct(r)),i&&!i[d]&&(i=ct(i,s)),N(function(s,o,u,a){var f,l,c,h=[],p=[],d=o.length,v=s||dt(t||"*",u.nodeType?[u]:u,[]),m=e&&(s||!t)?lt(v,h,e,u,a):v,g=n?i||(s?e:d||r)?[]:o:m;n&&n(m,g,u,a);if(r){f=lt(g,p),r(f,[],u,a),l=f.length;while(l--)if(c=f[l])g[p[l]]=!(m[p[l]]=c)}if(s){if(i||e){if(i){f=[],l=g.length;while(l--)(c=g[l])&&f.push(m[l]=c);i(null,g=[],f,a)}l=g.length;while(l--)(c=g[l])&&(f=i?T.call(s,c):h[l])>-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a<s;a++)if(n=i.relative[e[a].type])h=[at(ft(h),n)];else{n=i.filter[e[a].type].apply(null,e[a].matches);if(n[d]){r=++a;for(;r<s;r++)if(i.relative[e[r].type])break;return ct(a>1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a<r&&ht(e.slice(a,r)),r<s&&ht(e=e.slice(r)),r<s&&e.join(""))}h.push(n)}return ft(h)}function pt(e,t){var r=t.length>0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r<i;r++)nt(e,t[r],n);return n}function vt(e,t,n,r,s){var o,u,f,l,c,h=ut(e),p=h.length;if(!r&&h.length===1){u=h[0]=h[0].slice(0);if(u.length>2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;t<n;t++)if(this[t]===e)return t;return-1},N=function(e,t){return e[d]=t==null||t,e},C=function(){var e={},t=[];return N(function(n,r){return t.push(n)>i.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="<a href='#'></a>",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="<select></select>";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="<a name='"+d+"'></a><div name='"+d+"'></div>",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:st(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:st(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}},f=y.compareDocumentPosition?function(e,t){return e===t?(l=!0,0):(!e.compareDocumentPosition||!t.compareDocumentPosition?e.compareDocumentPosition:e.compareDocumentPosition(t)&4)?-1:1}:function(e,t){if(e===t)return l=!0,0;if(e.sourceIndex&&t.sourceIndex)return e.sourceIndex-t.sourceIndex;var n,r,i=[],s=[],o=e.parentNode,u=t.parentNode,a=o;if(o===u)return ot(e,t);if(!o)return-1;if(!u)return 1;while(a)i.unshift(a),a=a.parentNode;a=u;while(a)s.unshift(a),a=a.parentNode;n=i.length,r=s.length;for(var f=0;f<n&&f<r;f++)if(i[f]!==s[f])return ot(i[f],s[f]);return f===n?ot(e,s[f],-1):ot(i[f],t,1)},[0,0].sort(f),h=!l,nt.uniqueSort=function(e){var t,n=[],r=1,i=0;l=h,e.sort(f);if(l){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e},nt.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},a=nt.compile=function(e,t){var n,r=[],i=[],s=A[d][e+" "];if(!s){t||(t=ut(e)),n=t.length;while(n--)s=ht(t[n]),s[d]?r.push(s):i.push(s);s=A(e,pt(i,r))}return s},g.querySelectorAll&&function(){var e,t=vt,n=/'|\\/g,r=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,i=[":focus"],s=[":active"],u=y.matchesSelector||y.mozMatchesSelector||y.webkitMatchesSelector||y.oMatchesSelector||y.msMatchesSelector;K(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="<p test=''></p>",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="<input type='hidden'/>",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t<n;t++)if(v.contains(u[t],this))return!0});o=this.pushStack("","find",e);for(t=0,n=this.length;t<n;t++){r=o.length,v.find(e,this[t],o);if(t>0)for(i=r;i<o.length;i++)for(s=0;s<r;s++)if(o[s]===o[i]){o.splice(i--,1);break}}return o},has:function(e){var t,n=v(e,this),r=n.length;return this.filter(function(){for(t=0;t<r;t++)if(v.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1),"not",e)},filter:function(e){return this.pushStack(ft(this,e,!0),"filter",e)},is:function(e){return!!e&&(typeof e=="string"?st.test(e)?v(e,this.context).index(this[0])>=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r<i;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&n.nodeType!==11){if(o?o.index(n)>-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/<tbody/i,gt=/<|&#?\w+;/,yt=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,wt=new RegExp("<(?:"+ct+")[\\s/>]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,Nt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X<div>","</div>"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1></$2>");try{for(;r<i;r++)n=this[r]||{},n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),n.innerHTML=e);n=0}catch(s){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){return ut(this[0])?this.length?this.pushStack(v(v.isFunction(e)?e():e),"replaceWith",e):this:v.isFunction(e)?this.each(function(t){var n=v(this),r=n.html();n.replaceWith(e.call(this,t,r))}):(typeof e!="string"&&(e=v(e).detach()),this.each(function(){var t=this.nextSibling,n=this.parentNode;v(this).remove(),t?v(t).before(e):v(n).append(e)}))},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=[].concat.apply([],e);var i,s,o,u,a=0,f=e[0],l=[],c=this.length;if(!v.support.checkClone&&c>1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a<c;a++)r.call(n&&v.nodeName(this[a],"table")?Lt(this[a],"tbody"):this[a],a===u?o:v.clone(o,!0,!0))}o=s=null,l.length&&v.each(l,function(e,t){t.src?v.ajax?v.ajax({url:t.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):v.error("no ajax"):v.globalEval((t.text||t.textContent||t.innerHTML||"").replace(Tt,"")),t.parentNode&&t.parentNode.removeChild(t)})}return this}}),v.buildFragment=function(e,n,r){var s,o,u,a=e[0];return n=n||i,n=!n.nodeType&&n[0]||n,n=n.ownerDocument||n,e.length===1&&typeof a=="string"&&a.length<512&&n===i&&a.charAt(0)==="<"&&!bt.test(a)&&(v.support.checkClone||!St.test(a))&&(v.support.html5Clone||!wt.test(a))&&(o=!0,s=v.fragments[a],u=s!==t),s||(s=n.createDocumentFragment(),v.clean(e,n,s,r),o&&(v.fragments[a]=u&&s)),{fragment:s,cacheable:o}},v.fragments={},v.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){v.fn[e]=function(n){var r,i=0,s=[],o=v(n),u=o.length,a=this.length===1&&this[0].parentNode;if((a==null||a&&a.nodeType===11&&a.childNodes.length===1)&&u===1)return o[t](this[0]),this;for(;i<u;i++)r=(i>0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1></$2>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]==="<table>"&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("<div>").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r<i;r++)n=e[r],Vn[n]=Vn[n]||[],Vn[n].unshift(t)},prefilter:function(e,t){t?Xn.unshift(e):Xn.push(e)}}),v.Tween=Yn,Yn.prototype={constructor:Yn,init:function(e,t,n,r,i,s){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=s||(v.cssNumber[n]?"":"px")},cur:function(){var e=Yn.propHooks[this.prop];return e&&e.get?e.get(this):Yn.propHooks._default.get(this)},run:function(e){var t,n=Yn.propHooks[this.prop];return this.options.duration?this.pos=t=v.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Yn.propHooks._default.set(this),this}},Yn.prototype.init.prototype=Yn.prototype,Yn.propHooks={_default:{get:function(e){var t;return e.elem[e.prop]==null||!!e.elem.style&&e.elem.style[e.prop]!=null?(t=v.css(e.elem,e.prop,!1,""),!t||t==="auto"?0:t):e.elem[e.prop]},set:function(e){v.fx.step[e.prop]?v.fx.step[e.prop](e):e.elem.style&&(e.elem.style[v.cssProps[e.prop]]!=null||v.cssHooks[e.prop])?v.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},Yn.propHooks.scrollTop=Yn.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},v.each(["toggle","show","hide"],function(e,t){var n=v.fn[t];v.fn[t]=function(r,i,s){return r==null||typeof r=="boolean"||!e&&v.isFunction(r)&&v.isFunction(i)?n.apply(this,arguments):this.animate(Zn(t,!0),r,i,s)}}),v.fn.extend({fadeTo:function(e,t,n,r){return this.filter(Gt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=v.isEmptyObject(e),s=v.speed(t,n,r),o=function(){var t=Kn(this,v.extend({},e),s);i&&t.stop(!0)};return i||s.queue===!1?this.each(o):this.queue(s.queue,o)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return typeof e!="string"&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=e!=null&&e+"queueHooks",s=v.timers,o=v._data(this);if(n)o[n]&&o[n].stop&&i(o[n]);else for(n in o)o[n]&&o[n].stop&&Wn.test(n)&&i(o[n]);for(n=s.length;n--;)s[n].elem===this&&(e==null||s[n].queue===e)&&(s[n].anim.stop(r),t=!1,s.splice(n,1));(t||!r)&&v.dequeue(this,e)})}}),v.each({slideDown:Zn("show"),slideUp:Zn("hide"),slideToggle:Zn("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){v.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),v.speed=function(e,t,n){var r=e&&typeof e=="object"?v.extend({},e):{complete:n||!n&&t||v.isFunction(e)&&e,duration:e,easing:n&&t||t&&!v.isFunction(t)&&t};r.duration=v.fx.off?0:typeof r.duration=="number"?r.duration:r.duration in v.fx.speeds?v.fx.speeds[r.duration]:v.fx.speeds._default;if(r.queue==null||r.queue===!0)r.queue="fx";return r.old=r.complete,r.complete=function(){v.isFunction(r.old)&&r.old.call(this),r.queue&&v.dequeue(this,r.queue)},r},v.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},v.timers=[],v.fx=Yn.prototype.init,v.fx.tick=function(){var e,n=v.timers,r=0;qn=v.now();for(;r<n.length;r++)e=n[r],!e()&&n[r]===e&&n.splice(r--,1);n.length||v.fx.stop(),qn=t},v.fx.timer=function(e){e()&&v.timers.push(e)&&!Rn&&(Rn=setInterval(v.fx.tick,v.fx.interval))},v.fx.interval=13,v.fx.stop=function(){clearInterval(Rn),Rn=null},v.fx.speeds={slow:600,fast:200,_default:400},v.fx.step={},v.expr&&v.expr.filters&&(v.expr.filters.animated=function(e){return v.grep(v.timers,function(t){return e===t.elem}).length});var er=/^(?:body|html)$/i;v.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){v.offset.setOffset(this,e,t)});var n,r,i,s,o,u,a,f={top:0,left:0},l=this[0],c=l&&l.ownerDocument;if(!c)return;return(r=c.body)===l?v.offset.bodyOffset(l):(n=c.documentElement,v.contains(n,l)?(typeof l.getBoundingClientRect!="undefined"&&(f=l.getBoundingClientRect()),i=tr(c),s=n.clientTop||r.clientTop||0,o=n.clientLeft||r.clientLeft||0,u=i.pageYOffset||n.scrollTop,a=i.pageXOffset||n.scrollLeft,{top:f.top+u-s,left:f.left+a-o}):f)},v.offset={bodyOffset:function(e){var t=e.offsetTop,n=e.offsetLeft;return v.support.doesNotIncludeMarginInBodyOffset&&(t+=parseFloat(v.css(e,"marginTop"))||0,n+=parseFloat(v.css(e,"marginLeft"))||0),{top:t,left:n}},setOffset:function(e,t,n){var r=v.css(e,"position");r==="static"&&(e.style.position="relative");var i=v(e),s=i.offset(),o=v.css(e,"top"),u=v.css(e,"left"),a=(r==="absolute"||r==="fixed")&&v.inArray("auto",[o,u])>-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file
diff --git a/rpki/gui/app/templates/404.html b/rpki/gui/app/templates/404.html
new file mode 100644
index 00000000..76ef3aee
--- /dev/null
+++ b/rpki/gui/app/templates/404.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Page Not Found</h1>
+</div>
+
+<div class="alert alert-error">
+ <strong>Whoops!</strong> I could not find the page you requested.
+</div>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/500.html b/rpki/gui/app/templates/500.html
new file mode 100644
index 00000000..216fe8ae
--- /dev/null
+++ b/rpki/gui/app/templates/500.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Internal Server Error</h1>
+</div>
+
+<div class="alert alert-error">
+ <strong>Whoops!</strong> The administrator has been notified of this error.
+</div>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/alert_confirm_clear.html b/rpki/gui/app/templates/app/alert_confirm_clear.html
new file mode 100644
index 00000000..5d7fcf04
--- /dev/null
+++ b/rpki/gui/app/templates/app/alert_confirm_clear.html
@@ -0,0 +1,21 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Delete all alerts</h1>
+</div>
+
+<div class="row-fluid">
+ <div class="alert">
+ Please confirm that you would like to delete all alerts.
+ </div>
+ <form method="POST">
+ {% csrf_token %}
+ <div class="form-actions">
+ <button class="btn btn-danger" type="submit"><i class="icon-trash"></i> Delete All</button>
+ <a class="btn" href="{% url "alert-list" %}">Cancel</a>
+ </div>
+ </form>
+</div>
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/alert_confirm_delete.html b/rpki/gui/app/templates/app/alert_confirm_delete.html
new file mode 100644
index 00000000..78c84917
--- /dev/null
+++ b/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/rpki/gui/app/templates/app/alert_detail.html b/rpki/gui/app/templates/app/alert_detail.html
new file mode 100644
index 00000000..b3a73b7e
--- /dev/null
+++ b/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/rpki/gui/app/templates/app/alert_list.html b/rpki/gui/app/templates/app/alert_list.html
new file mode 100644
index 00000000..dd0530e4
--- /dev/null
+++ b/rpki/gui/app/templates/app/alert_list.html
@@ -0,0 +1,31 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% 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>
+
+<div class='row-fluid'>
+ <a class="btn btn-danger" href="{% url 'alert-clear-all' %}"><i class='icon-trash'></i> Delete All</a>
+</div>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/app_base.html b/rpki/gui/app/templates/app/app_base.html
new file mode 100644
index 00000000..4fb5f731
--- /dev/null
+++ b/rpki/gui/app/templates/app/app_base.html
@@ -0,0 +1,31 @@
+{% 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. #}
+
+{% block sidebar %}
+
+<h2>{{ request.session.handle }}</h2>
+
+{# 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>
+ <li><a href="{% url "rpki.gui.app.views.user_list" %}" title="manage users"><i class="icon-user"></i> web users</a></li>
+ <li><a href="{% url "rpki.gui.app.views.resource_holder_list" %}" title="manage resource holders"><i class="icon-user"></i> resource holders</a></li>
+ <li><a href="{% url "rpki.gui.app.views.client_list" %}" title="manage repository clients">repository clients</a></li>
+{% endif %}
+{% block sidebar_extra %}{% endblock %}
+</ul>
+
+{% endblock sidebar %}
diff --git a/rpki/gui/app/templates/app/app_confirm_delete.html b/rpki/gui/app/templates/app/app_confirm_delete.html
new file mode 100644
index 00000000..7c35a733
--- /dev/null
+++ b/rpki/gui/app/templates/app/app_confirm_delete.html
@@ -0,0 +1,21 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>{{ form_title }}</h1>
+</div>
+
+<div class='alert alert-block'>
+ <h4>Warning!</h4>
+ <strong>Please confirm</strong> that you would like to delete this object.
+</div>
+
+<form method='POST' action="">
+ {% csrf_token %}
+ {{ form }}
+ <div class="form-actions">
+ <input class='btn btn-danger' value='Delete' type='submit'>
+ <a class='btn' href="{{ cancel_url }}">Cancel</a>
+ </div>
+</form>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/app_form.html b/rpki/gui/app/templates/app/app_form.html
new file mode 100644
index 00000000..b6ab60a2
--- /dev/null
+++ b/rpki/gui/app/templates/app/app_form.html
@@ -0,0 +1,19 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class="page-header">
+ <h1>{{ form_title }}</h1>
+</div>
+
+{# allow this template to be subclassed to fill in extra information, such as warnings #}
+{% block form_info %}{% endblock form_info %}
+
+<form method="POST" action="" enctype="multipart/form-data" class="form-horizontal">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class="form-actions">
+ <input class='btn btn-primary' type='submit' value='Save'>
+ <a class='btn' href="{{ cancel_url }}">Cancel</a>
+ </div>
+</form>
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/bootstrap_form.html b/rpki/gui/app/templates/app/bootstrap_form.html
new file mode 100644
index 00000000..c6fd5424
--- /dev/null
+++ b/rpki/gui/app/templates/app/bootstrap_form.html
@@ -0,0 +1,26 @@
+{% if form.non_field_errors %}
+<div class="alert alert-block alert-error">
+ {{ form.non_field_errors }}
+</div>
+{% endif %}
+
+{% for field in form %}
+
+{% if field.is_hidden %}
+{{ field }}
+{% else %}
+<div class="control-group {% if field.errors %}error{% endif %}">
+ <label class="control-label" for="{{ field.html_name }}">{{ field.label }}</label>
+ <div class="controls">
+ {{ field }}
+ {% if field.help_text %}
+ <span class="help-inline">{{ field.help_text }}</span>
+ {% endif %}
+ {% if field.errors %}
+ <span class="help-inline">{{ field.errors }}</span>
+ {% endif %}
+ </div>
+</div>
+{% endif %}
+
+{% endfor %}
diff --git a/rpki/gui/app/templates/app/child_detail.html b/rpki/gui/app/templates/app/child_detail.html
new file mode 100644
index 00000000..8178e179
--- /dev/null
+++ b/rpki/gui/app/templates/app/child_detail.html
@@ -0,0 +1,48 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Child: {{ object.handle }}</h1>
+</div>
+
+<div class='row-fluid'>
+ <p><strong>Valid until</strong> {{ object.valid_until }}
+</div>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <strong>Addresses</strong>
+ {% if object.address_ranges.all %}
+ <ul class='unstyled'>
+ {% for a in object.address_ranges.all %}
+ <li>{{ a.as_resource_range }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p style='font-style:italic'>none</p>
+ {% endif %}
+ </div>
+ <div class='span6'>
+ <strong>ASNs</strong>
+ {% if object.asns.all %}
+ <ul class='unstyled'>
+ {% for a in object.asns.all %}
+ <li>{{ a.as_resource_range }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p style='font-style:italic'>none</p>
+ {% endif %}
+ </div>
+</div>
+
+{% block action %}
+<a class='btn' href="{% url "rpki.gui.app.views.child_edit" object.pk %}" title='Edit this child'><i class="icon-edit"></i> Edit</a>
+<a class='btn' href="{% url "rpki.gui.app.views.child_add_asn" object.pk %}" title='Delegate an ASN to this child'><i class="icon-plus-sign"></i> AS</a>
+<a class='btn' href="{% url "rpki.gui.app.views.child_add_prefix" object.pk %}" title='Delegate a prefix to this child'><i class="icon-plus-sign"></i> Prefix</a>
+<a class='btn' href="{% url "rpki.gui.app.views.child_response" object.pk %}" title='Download XML file to send to child'><i class="icon-download"></i> Export</a>
+<a class="btn" href="{% url "rpki.gui.app.views.child_delete" object.pk %}" title="Delete this child"><i class="icon-trash"></i> Delete</a>
+{% endblock %}
+
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/client_detail.html b/rpki/gui/app/templates/app/client_detail.html
new file mode 100644
index 00000000..3117e859
--- /dev/null
+++ b/rpki/gui/app/templates/app/client_detail.html
@@ -0,0 +1,25 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Repository Client: {{ object.handle }}</h1>
+</div>
+
+<table class="table">
+ <tr>
+ <td>Name</td>
+ <td>{{ object.handle }} </td>
+ </tr>
+ <tr>
+ <td>SIA</td>
+ <td>{{ object.sia_base }}</td>
+ </tr>
+</table>
+
+{% block action %}
+<a class="btn" href="{% url "client-export" object.pk %}" title="Download XML response to send to publication client"><i class="icon-download"></i> Export</a>
+<a class="btn" href="{% url "rpki.gui.app.views.client_delete" object.pk %}"><i class="icon-trash"></i> Delete</a>
+{% endblock action %}
+
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/client_list.html b/rpki/gui/app/templates/app/client_list.html
new file mode 100644
index 00000000..12987c53
--- /dev/null
+++ b/rpki/gui/app/templates/app/client_list.html
@@ -0,0 +1,22 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Repository Clients</h1>
+</div>
+<table class="table table-striped">
+ <thead><tr><th>Handle</th><th>Action</th></tr></thead>
+ <tbody>
+ {% for client in object_list %}
+ <tr>
+ <td><a href="{% url "rpki.gui.app.views.client_detail" client.pk %}">{{ client.handle }}</a></td>
+ <td>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.client_delete" client.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+<a class="btn" href="{% url "rpki.gui.app.views.client_import" %}"><i class="icon-upload"></i> Import</a>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/conf_empty.html b/rpki/gui/app/templates/app/conf_empty.html
new file mode 100644
index 00000000..efe06f14
--- /dev/null
+++ b/rpki/gui/app/templates/app/conf_empty.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% load url from future %}
+
+{% block content %}
+
+{% if request.user.is_superuser %}
+<div class="alert alert-info">
+There are currently no resource holders on this system.
+</div>
+<a class="btn" href="{% url "rpki.gui.app.views.resource_holder_create" %}" title="create a new resource holder"><i class="icon-plus-sign"></i> Create</a>
+{% else %}
+<div class="alert alert-error">
+Your account does not have permission to manage any resource handles on this server. Please contact your portal-gui adminstrator.
+</div>
+{% endif %}
+
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/conf_list.html b/rpki/gui/app/templates/app/conf_list.html
new file mode 100644
index 00000000..dce6d59e
--- /dev/null
+++ b/rpki/gui/app/templates/app/conf_list.html
@@ -0,0 +1,17 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Handle List</h1>
+</div>
+
+<p>Please select a handle.</p>
+
+<ul>
+ {% for c in conf_list %}
+ <li><a href="{% url "rpki.gui.app.views.conf_select" %}?handle={{ c.handle }}&next={{ next_url }}">{{ c.handle }}</a></li>
+ {% endfor %}
+</ul>
+
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/dashboard.html b/rpki/gui/app/templates/app/dashboard.html
new file mode 100644
index 00000000..65dbb90f
--- /dev/null
+++ b/rpki/gui/app/templates/app/dashboard.html
@@ -0,0 +1,230 @@
+{% extends "app/app_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 %}
+
+{% block sidebar_extra %}
+ <li class="divider"></li>
+ <li><a href="{% url "rpki.gui.app.views.conf_export" %}" title="download XML identity to send to parent">
+ {#<i class="icon-download"></i> #}export identity</a></li>
+{% endblock sidebar_extra %}
+
+{% block content %}
+<div class='row-fluid'>
+ <div class='span6'>
+ <div class="page-header">
+ <h1>Resources</h1>
+ </div>
+
+ <table class='table table-condensed-table table-striped'>
+ <tr>
+ <th>Resource</th>
+ <th>Valid Until</th>
+ <th>Parent</th>
+ </tr>
+
+ {% for object in asns %}
+ <tr>
+ <td>{{ object }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>
+ {% if object.cert.parent %}
+ <a href="{{ object.cert.parent.get_absolute_url }}">{{ object.cert.parent.handle }}</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+
+ {% for object in prefixes %}
+ <tr>
+ <td>{{ object.as_resource_range }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>
+ {% if object.cert.parent %}
+ <a href="{{ object.cert.parent.get_absolute_url }}">{{ object.cert.parent.handle }}</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+
+ {% if prefixes_v6 %}
+ {% for object in prefixes_v6 %}
+ <tr>
+ <td>{{ object.as_resource_range }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>
+ {% if object.cert.parent %}
+ <a href="{{ object.cert.parent.get_absolute_url }}">{{ object.cert.parent.handle }}</a>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ <a class='btn' href="{% url "rpki.gui.app.views.refresh" %}" title="refresh resource list from rpkid"><i class="icon-refresh"></i> refresh</a></li>
+ </div>
+ <div class='span6'>
+ <h2>Unallocated Resources</h2>
+ <p>The following resources have not been allocated to a child, nor appear in a ROA.
+
+ {% if unused_asns %}
+ <h3>ASNs</h3>
+ <ul>
+ {% for asn in unused_asns %}
+ <li>AS{{ asn }}
+ {% endfor %} <!-- ASNs -->
+ </ul>
+ {% endif %}
+
+ {% if unused_prefixes %}
+ <h3>IPv4</h3>
+ <table class="table table-condensed table-striped">
+ <tr><th>Prefix</th><th>Action</th></tr>
+ {% for addr in unused_prefixes %}
+ <tr>
+ <td>{{ addr }}</td>
+ <td>
+ <a class="btn btn-mini" title="Create ROA using this prefix" href="{% url "rpki.gui.app.views.roa_create_multi" %}?roa={{ addr }}"><i class="icon-plus-sign"></i> ROA</a>
+ </td>
+ </tr>
+ {% endfor %} <!-- addrs -->
+ </table>
+ {% endif %}
+
+ {% if unused_prefixes_v6 %}
+ <h3>IPv6</h3>
+ <table class="table table-condensed table-striped">
+ <tr><th>Prefix</th><th></th></tr>
+ {% for addr in unused_prefixes_v6 %}
+ <tr>
+ <td>{{ addr }}</td>
+ <td>
+ <a class="btn btn-mini" title='create roa using this prefix' href="{% url "rpki.gui.app.views.roa_create_multi" %}?roa={{ addr }}"><i class="icon-plus-sign"></i> ROA</a>
+ </td>
+ </tr>
+ {% endfor %} <!-- addrs -->
+ </table>
+ {% endif %}
+
+ </div><!-- /span -->
+</div><!-- /row -->
+
+<div class="row-fluid">
+ <div class="span6">
+<div class="page-header">
+ <h1>ROAs</h1>
+</div>
+<table class="table table-condensed table-striped">
+ <tr><th>Prefix</th><th>Max Length</th><th>AS</th><th></th></tr>
+ {% for roa in conf.roas %}
+ <tr>
+ <!-- each roa request has a single roa request prefix object associated -->
+ <td>{{ roa.prefixes.all.0.as_roa_prefix }}</td>
+ <td>{{ roa.prefixes.all.0.max_prefixlen }}</td>
+ <td>{{ roa.asn }}</td>
+ <td>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.roa_detail" roa.pk %}" title="Detail"><i class="icon-info-sign"></i></a>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.roa_delete" roa.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ <a class="btn btn-mini" href="{% url "roa-clone" roa.pk %}" title="create another ROA for this prefix"><i class="icon-repeat"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+<a class="btn" href="{% url "rpki.gui.app.views.roa_create_multi" %}"><i class="icon-plus-sign"></i> Create</a>
+<a class="btn" href="{% url "roa-import" %}" title="import a CSV file containing ROAs"><i class="icon-upload"></i> Import</a>
+<a class="btn" href="{% url "roa-export" %}" title="download a CSV file containing ROAs"><i class="icon-download"></i> Export</a>
+</div>
+
+ <div class="span6">
+<div class="page-header">
+ <h1>Ghostbusters</h1>
+</div>
+<table class="table table-condensed table-striped">
+ <tr><th>Full Name</th><th>Organization</th><th>Email</th><th>Telephone</th><th></th></tr>
+ {% for gbr in conf.ghostbusters %}
+ <tr>
+ <td>{{ gbr.full_name }}</td>
+ <td>{{ gbr.organization }}</td>
+ <td>{{ gbr.email_address }}</td>
+ <td>{{ gbr.telephone }}</td>
+ <td>
+ <a class="btn btn-mini" href="{% url "gbr-detail" gbr.pk %}" title="View"><i class="icon-info-sign"></i></a>
+ <a class="btn btn-mini" href="{% url "gbr-edit" gbr.pk %}" title="Edit"><i class="icon-edit"></i></a>
+ <a class="btn btn-mini" href="{% url "gbr-delete" gbr.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+<a class="btn" href="{% url "gbr-create" %}"><i class="icon-plus-sign"></i> Create</a>
+</div><!-- /span -->
+</div><!-- /row -->
+
+<div class="row-fluid">
+ <div class="span6">
+ <div class="page-header">
+ <h1>Children</h1>
+ </div>
+<table class="table table-condensed table-striped">
+ <tr><th>Handle</th><th></th>
+ {% for child in conf.children %}
+ <tr>
+ <td><a href="{{ child.get_absolute_url }}">{{ child.handle }}</a></td>
+ <td>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.child_delete" child.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ <div class="row-fluid">
+ <div class='span6'>
+ <a class="btn" href="{% url "rpki.gui.app.views.child_import" %}" title="Import XML request from Child"><i class="icon-upload"></i> Child</a>
+ <a class="btn" href="{% url "import-asns" %}" title="Import CSV file containing ASN delgations to children"><i class="icon-upload"></i> ASNs</a>
+ <a class="btn" href="{% url "import-prefixes" %}" title="import CSV file containing prefix delgations to children"><i class="icon-upload"></i> Prefixes</a>
+ </div>
+ </div>
+ <div class="row-fluid">
+ <div class='span6'>
+ <a class="btn" href="{% url "export-asns" %}" title="Export CSV file containing ASN delgations to children"><i class="icon-download"></i> ASNs</a>
+ <a class="btn" href="{% url "export-prefixes" %}" title="Export CSV file containing prefix delgations to children"><i class="icon-download"></i> Prefixes</a>
+ </div>
+ </div>
+ </div><!-- /span -->
+ <div class="span6">
+ <div class="page-header">
+ <h1>Parents</h1>
+ </div>
+ <table class="table table-condensed table-striped">
+ <tr><th>Handle</th><th></th></tr>
+ {% for parent in conf.parents %}
+ <tr>
+ <td><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a></td>
+ <td>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.parent_delete" parent.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ <a class="btn" href="{% url "rpki.gui.app.views.parent_import" %}"><i class="icon-upload"></i> Import</a>
+ </div><!-- /span -->
+</div><!-- /row -->
+
+<div class="row-fluid">
+ <div class="span6">
+ <div class="page-header">
+ <h1>Repositories</h1>
+ </div>
+<table class="table table-condensed table-striped">
+ <tr><th>Handle</th><th></th></tr>
+ {% for repo in conf.repositories %}
+ <tr>
+ <td><a href="{{ repo.get_absolute_url }}">{{ repo.handle }}</a></td>
+ <td>
+ <a class="btn btn-mini" href="{% url "rpki.gui.app.views.repository_delete" repo.pk %}" title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ <a class="btn" href="{% url "rpki.gui.app.views.repository_import" %}"><i class="icon-upload"></i> Import</a>
+ </div><!-- /span -->
+</div><!-- /row -->
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html b/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html
new file mode 100644
index 00000000..76b1d25a
--- /dev/null
+++ b/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html
@@ -0,0 +1,20 @@
+{% extends "app/ghostbuster_detail.html" %}
+
+{% block extra %}
+
+<div class='alert-message block-message warning'>
+ <p>
+ <strong>Please confirm</strong> that you really want to delete by clicking Delete.
+
+ <div class='alert-actions'>
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <input class='btn danger' type='submit' value='Delete' />
+ <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a>
+ </form>
+ </div>
+</div>
+
+{% endblock %}
+
+<!-- vim:set sw=2: -->
diff --git a/rpki/gui/app/templates/app/ghostbusterrequest_detail.html b/rpki/gui/app/templates/app/ghostbusterrequest_detail.html
new file mode 100644
index 00000000..296f0f16
--- /dev/null
+++ b/rpki/gui/app/templates/app/ghostbusterrequest_detail.html
@@ -0,0 +1,64 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Ghostbuster Request</h1>
+</div>
+
+<table class='table table-striped table-condensed'>
+ <tr><td>Full Name</td><td>{{ object.full_name }}</td></tr>
+
+ {% if object.honorific_prefix %}
+ <tr><td >Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr>
+ {% endif %}
+
+ {% if object.organization %}
+ <tr><td >Organization</td><td>{{ object.organization }}</td></tr>
+ {% endif %}
+
+ {% if object.telephone %}
+ <tr><td >Telephone</td><td>{{ object.telephone }}</td></tr>
+ {% endif %}
+
+ {% if object.email_address %}
+ <tr><td >Email</td><td>{{ object.email_address }}</td></tr>
+ {% endif %}
+
+ {% if object.box %}
+ <tr><td >P.O. Box</td><td>{{ object.box }}</td></tr>
+ {% endif %}
+
+ {% if object.extended %}
+ <tr><td >Extended Address</td><td>{{ object.extended }}</td></tr>
+ {% endif %}
+
+ {% if object.street %}
+ <tr><td >Street Address</td><td>{{ object.street }}</td></tr>
+ {% endif %}
+
+ {% if object.city %}
+ <tr><td >City</td><td>{{ object.city }}</td></tr>
+ {% endif %}
+
+ {% if object.region %}
+ <tr><td >Region</td><td>{{ object.region }}</td></tr>
+ {% endif %}
+
+ {% if object.code %}
+ <tr><td >Postal Code</td><td>{{ object.code }}</td></tr>
+ {% endif %}
+
+ {% if object.country %}
+ <tr><td >Country</td><td>{{ object.country }}</td></tr>
+ {% endif %}
+
+</table>
+
+{% block action %}
+{# the roarequest_confirm_delete template will override this section #}
+<a class="btn" href="{% url "gbr-edit" object.pk %}"><i class="icon-edit"></i> Edit</a>
+<a class="btn" href="{% url "gbr-delete" object.pk %}"><i class="icon-trash"></i> Delete</a>
+{% endblock action %}
+
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/import_resource_form.html b/rpki/gui/app/templates/app/import_resource_form.html
new file mode 100644
index 00000000..e446d344
--- /dev/null
+++ b/rpki/gui/app/templates/app/import_resource_form.html
@@ -0,0 +1,9 @@
+{% extends "app/app_form.html" %}
+
+{% block form_info %}
+<div class="alert alert-block alert-warning">
+ <b>Warning!</b> All existing resources of this type currently in the
+ database <b>will be deleted</b> and replaced with the contents of the CSV
+ file you are uploading.
+</div>
+{% endblock form_info %}
diff --git a/rpki/gui/app/templates/app/object_confirm_delete.html b/rpki/gui/app/templates/app/object_confirm_delete.html
new file mode 100644
index 00000000..c4af9b26
--- /dev/null
+++ b/rpki/gui/app/templates/app/object_confirm_delete.html
@@ -0,0 +1,21 @@
+{% extends parent_template %}
+{% comment %}
+Since Django templates do not support multiple inheritance, we simluate it by
+dynamically extending from the *_detail.html template for a concrete object
+type. The *DeleteView classes should set a "parent_template" variable which is
+string specifying the concrete template to inherit from.
+{% endcomment %}
+{% load url from future %}
+
+{% block action %}
+<div class="alert alert-warning alert-block">
+ <h4>Warning!</h4>
+ Please confirm that you would like to delete this object
+</div>
+
+<form action='' method='POST'>
+ {% csrf_token %}
+ <input class='btn btn-danger' type='submit' value='Delete'>
+ <a class='btn' href="{% url "rpki.gui.app.views.dashboard" %}">Cancel</a>
+</form>
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/parent_detail.html b/rpki/gui/app/templates/app/parent_detail.html
new file mode 100644
index 00000000..4dd1842f
--- /dev/null
+++ b/rpki/gui/app/templates/app/parent_detail.html
@@ -0,0 +1,67 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Parent: {{ object.handle }}</h1>
+</div>
+
+<table class="table table-striped table-condensed">
+ <tr>
+ <td>service_uri</td>
+ <td>{{ object.service_uri }}</td>
+ </tr>
+ <tr>
+ <td>parent_handle</td>
+ <td>{{ object.parent_handle }}</td>
+ </tr>
+ <tr>
+ <td>child_handle</td>
+ <td>{{ object.child_handle }}</td>
+ </tr>
+ <tr>
+ <td>repository_type</td>
+ <td>{{ object.repository_type }}</td>
+ </tr>
+ <tr>
+ <td>referrer</td>
+ <td>{{ object.referrer }}</td>
+ </tr>
+ <tr>
+ <td>ta validity period</td>
+ <td>{{ object.ta.getNotBefore }} - {{ object.ta.getNotAfter }}</td>
+ </tr>
+</table>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <h3>Delegated Addresses</h3>
+ <ul class='unstyled'>
+ {% for c in object.certs.all %}
+ {% for a in c.address_ranges.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% for a in c.address_ranges_v6.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% endfor %}
+ </ul>
+ </div>
+ <div class='span6'>
+ <h3>Delegated ASNs</h3>
+ <ul class='unstyled'>
+ {% for c in object.certs.all %}
+ {% for a in c.asn_ranges.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% endfor %}
+ </ul>
+ </div>
+</div>
+
+{% block action %}
+<a class='btn' href='{% url "rpki.gui.app.views.parent_export" object.pk %}' title='Download XML to send to repository operator'><i class="icon-download"></i> Export</a>
+<a class="btn" href="{% url "rpki.gui.app.views.parent_delete" object.pk %}" title="Delete this parent"><i class="icon-trash"></i> Delete</a>
+{% endblock action %}
+
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/pubclient_list.html b/rpki/gui/app/templates/app/pubclient_list.html
new file mode 100644
index 00000000..1872e005
--- /dev/null
+++ b/rpki/gui/app/templates/app/pubclient_list.html
@@ -0,0 +1,10 @@
+{% extends "app/object_list.html" %}
+{% load url from future %}
+
+{% block actions %}
+<div class='actions'>
+ <a class='btn' href='{% url "rpki.gui.app.views.pubclient_import" %}'>Import</a>
+</div>
+{% endblock actions %}
+
+<!-- vim:set sw=2: -->
diff --git a/rpki/gui/app/templates/app/repository_detail.html b/rpki/gui/app/templates/app/repository_detail.html
new file mode 100644
index 00000000..92a43e54
--- /dev/null
+++ b/rpki/gui/app/templates/app/repository_detail.html
@@ -0,0 +1,19 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Repository: {{ object.handle }}</h1>
+</div>
+
+<table class="table">
+ <tr>
+ <td><strong>SIA</strong></td>
+ <td>{{ object.sia_base }}</td>
+ </tr>
+</table>
+
+{% block action %}
+<a class="btn" href="{% url "rpki.gui.app.views.repository_delete" object.pk %}" title="Delete this repository"><i class="icon-trash"></i> Delete</a>
+{% endblock action %}
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/resource_holder_list.html b/rpki/gui/app/templates/app/resource_holder_list.html
new file mode 100644
index 00000000..6525e74d
--- /dev/null
+++ b/rpki/gui/app/templates/app/resource_holder_list.html
@@ -0,0 +1,37 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Resource Holders</h1>
+</div>
+
+<p>
+This page lists all of the resource holders that are currently managed by this server.
+Note that this is distinct from the
+<a href="{% url "rpki.gui.app.views.user_list" %}">list of web interface users</a>.
+</p>
+
+<table class='table table-striped'>
+ <thead>
+ <tr>
+ <th>Handle</th>
+ <th>Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for conf in object_list %}
+ <tr>
+ <td>{{ conf.handle }}</td>
+ <td>
+ <a class='btn btn-small' href='{% url "rpki.gui.app.views.resource_holder_edit" conf.pk %}' title="Edit"><i class="icon-edit"></i></a>
+ <a class='btn btn-small' href='{% url "rpki.gui.app.views.resource_holder_delete" conf.pk %}' title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+<a class="btn" href="{% url "rpki.gui.app.views.resource_holder_create" %}"><i class="icon-plus-sign"></i> Create</a>
+{% endblock content %}
+{# vim: set ft=htmldjango: #}
diff --git a/rpki/gui/app/templates/app/roa_detail.html b/rpki/gui/app/templates/app/roa_detail.html
new file mode 100644
index 00000000..ec76579d
--- /dev/null
+++ b/rpki/gui/app/templates/app/roa_detail.html
@@ -0,0 +1,40 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+{% load app_extras %}
+
+{% block content %}
+<div class="page-header">
+ <h1>ROA Detail</h1>
+</div>
+
+<div class="row-fluid">
+ <div class="span6 well">
+ <table class="table">
+ <tr><th>Prefix</th><th>Max Length</th><th>AS</th></tr>
+ <tr>
+ <td>{{ object.prefixes.all.0.as_roa_prefix }}</td>
+ <td>{{ object.prefixes.all.0.max_prefixlen }}</td>
+ <td>{{ object.asn }}</td>
+ </tr>
+ </table>
+ </div>
+
+ <div class="span6">
+ <h3>Covered Routes</h3>
+ <p>This table lists currently announced routes which are covered by prefixes included in this ROA.
+ <table class="table">
+ <tr><th>Prefix</th><th>AS</th><th>Validity</th></tr>
+ {% for r in object.routes %}
+ <tr>
+ <td>{{ r.as_resource_range }}</td>
+ <td>{{ r.asn }}</td>
+ <td>{% validity_label r.status %}</td>
+ <td><a href="{{ r.get_absolute_url }}" title="view route detail"><i class="icon-info-sign"></i></a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+</div>
+
+<a class="btn" href="{% url "rpki.gui.app.views.roa_delete" object.pk %}"><i class="icon-trash"></i> Delete</a>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/roarequest_confirm_delete.html b/rpki/gui/app/templates/app/roarequest_confirm_delete.html
new file mode 100644
index 00000000..7dc3ec2b
--- /dev/null
+++ b/rpki/gui/app/templates/app/roarequest_confirm_delete.html
@@ -0,0 +1,59 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+{% load app_extras %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Delete ROA Request</h1>
+</div>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <div class='alert alert-block alert-warning'>
+ <p><strong>Please confirm</strong> that you would like to delete the
+ following ROA Request. The table to the right indicates how validation
+ status for matching routes may change.
+ </div>
+
+ <table class='table'>
+ <tr>
+ <th>Prefix</th>
+ <td>{{ object.prefixes.all.0.as_roa_prefix }}</td>
+ </tr>
+ <tr>
+ <th>Max Length</th>
+ <td>{{ object.prefixes.all.0.max_prefixlen }}</td>
+ </tr>
+ <tr>
+ <th>AS</th>
+ <td>{{ object.asn }}</td>
+ </tr>
+ </table>
+
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <input class='btn btn-danger' type='submit' value='Delete'/>
+ <a class='btn' href="{% url "rpki.gui.app.views.dashboard" %}">Cancel</a>
+ </form>
+ </div>
+
+ <div class='span6'>
+ <h2>Matching Routes</h2>
+
+ <table class='table table-striped table-condensed'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td>{% validity_label r.newstatus %}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div><!-- /span8 -->
+</div><!-- /row -->
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/roarequest_confirm_form.html b/rpki/gui/app/templates/app/roarequest_confirm_form.html
new file mode 100644
index 00000000..446bb6a4
--- /dev/null
+++ b/rpki/gui/app/templates/app/roarequest_confirm_form.html
@@ -0,0 +1,60 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Confirm ROA Request</h1>
+</div>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <div class='alert alert-block-message alert-warning'>
+ <p><strong>Please confirm</strong> that you would like to create the following ROA.
+ The accompanying table indicates how the validation status may change as a result.
+ </div>
+
+ <table class='table table-condensed table-striped'>
+ <tr>
+ <th>AS</th>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ </tr>
+ <tr>
+ <td>{{ asn }}</td>
+ <td>{{ prefix }}</td>
+ <td>{{ max_prefixlen }}</td>
+ </tr>
+ </table>
+
+ <form method='POST' action='{% url "rpki.gui.app.views.roa_create_confirm" %}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+
+ <div class='form-actions'>
+ <input class='btn btn-primary' type='submit' value='Create'/>
+ <a class='btn' href='{% url "rpki.gui.app.views.dashboard" %}'>Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div class='span6'>
+ <h2>Matched Routes</h2>
+
+ <table class='table table-striped table-condensed'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+</div>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html b/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html
new file mode 100644
index 00000000..4a06a4aa
--- /dev/null
+++ b/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html
@@ -0,0 +1,66 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+{% load app_extras %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Confirm ROA Requests</h1>
+</div>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <div class='alert alert-block-message alert-warning'>
+ <p><strong>Please confirm</strong> that you would like to create the following ROA(s).
+ The accompanying table indicates how the validation status may change as a result.
+ </div>
+
+ <table class='table table-condensed table-striped'>
+ <tr>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ <th>AS</th>
+ </tr>
+ {% for roa in roas %}
+ <tr>
+ <td>{{ roa.prefix }}</td>
+ <td>{{ roa.max_prefixlen }}</td>
+ <td>{{ roa.asn }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+
+ <form method='POST' action='{% url "rpki.gui.app.views.roa_create_multi_confirm" %}'>
+ {% csrf_token %}
+ {{ formset.management_form }}
+ {% for form in formset %}
+ {% include "app/bootstrap_form.html" %}
+ {% endfor %}
+
+ <div class='form-actions'>
+ <input class='btn btn-primary' type='submit' value='Create'/>
+ <a class='btn' href='{% url "rpki.gui.app.views.dashboard" %}'>Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div class='span6'>
+ <h2>Matched Routes</h2>
+
+ <table class='table table-striped table-condensed'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td>{% validity_label r.newstatus %}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+</div>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/roarequest_form.html b/rpki/gui/app/templates/app/roarequest_form.html
new file mode 100644
index 00000000..3a29131d
--- /dev/null
+++ b/rpki/gui/app/templates/app/roarequest_form.html
@@ -0,0 +1,50 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{# This form is used for creating a new ROA request #}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create ROA</h1>
+</div>
+
+<script src='{{ STATIC_URL }}js/jquery-1.8.3.min.js'></script>
+<script type='text/javascript'>
+ var f = function(){
+ var e = $("#route_table")
+ e.empty()
+ e.append('<tr><th>Prefix</th><th>AS</th></tr>')
+ $.getJSON('/api/v1/route/', {'prefix__in':$(this).val()}, function(data){
+ if (data.length == 1) {
+ $("#id_asn").val(data[0].asn)
+ }
+ for (var x in data) {
+ e.append('<tr><td>' + data[x].prefix + '</td><td>' + data[x].asn + '</td></tr>')
+ }
+ })
+ }
+
+ $(document).ready(function(){ $("#id_prefix").change(f) })
+</script>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class="form-actions">
+ <input class="btn" type="submit" value="Preview">
+ <a class="btn" href="{% url "rpki.gui.app.views.dashboard" %}">Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div class='span6'>
+ Routes matching your prefix:
+ <table class='table table-condensed table-striped' id='route_table'>
+ <tr><th>Prefix</th><th>AS</th></tr>
+ <!-- script above populates this table based upon prefix matches -->
+ </table>
+ </div>
+</div><!--row-->
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/roarequest_multi_form.html b/rpki/gui/app/templates/app/roarequest_multi_form.html
new file mode 100644
index 00000000..06d07943
--- /dev/null
+++ b/rpki/gui/app/templates/app/roarequest_multi_form.html
@@ -0,0 +1,28 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create ROAs</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {{ formset.management_form }}
+ {% for form in formset %}
+ <div class="controls controls-row">
+ {{ form.prefix }}
+ {{ form.max_prefixlen }}
+ {{ form.asn }}
+ <label class="checkbox inline span1">{{ form.DELETE }} Delete</label>
+ {% if form.errors %}<span class="help-inline">{{ form.errors }}</span>{% endif %}
+ {% if form.non_field_errors %}<span class="help-inline">{{ form.non_field_errors }}</span>{% endif %}
+ </div>
+ {% endfor %}
+
+ <div class="form-actions">
+ <input class="btn" type="submit" value="Preview">
+ <a class="btn" href="{% url "rpki.gui.app.views.dashboard" %}">Cancel</a>
+ </div>
+</form>
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/route_detail.html b/rpki/gui/app/templates/app/route_detail.html
new file mode 100644
index 00000000..84add4a8
--- /dev/null
+++ b/rpki/gui/app/templates/app/route_detail.html
@@ -0,0 +1,58 @@
+{% extends "app/app_base.html" %}
+{% load app_extras %}
+{% load bootstrap_pager %}
+
+{# template for displaying the list of ROAs covering a specific route #}
+
+{% block content %}
+<div class="page-header">
+ <h1>Route Detail</h1>
+</div>
+
+<div class="row-fluid">
+ <div class="span12 well">
+ <table class="table table-striped table-condensed">
+ <thead>
+ <tr><th>Prefix</th><th>AS</th><th>Validity</th></tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{{ object.as_resource_range }}</td>
+ <td>{{ object.asn }}</td>
+ <td>{% validity_label object.status %}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+
+<div class="row-fluid">
+ <div class="span12">
+ <p>The table below lists all ROAs which cover the route described above.
+
+ <table class="table table-striped table-condensed">
+ <thead>
+ <tr>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ <th>ASN</th>
+ <th>Expires</th>
+ <th>URI</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for pfx in roa_prefixes %}
+ <tr>
+ <td>{{ pfx.as_resource_range }}</td>
+ <td>{{ pfx.max_length }}</td>
+ <td>{{ pfx.roas.all.0.asid }}</td>
+ <td>{{ pfx.roas.all.0.not_after }}</td>
+ <td>{{ pfx.roas.all.0.repo.uri }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% bootstrap_pager request roa_prefixes %}
+ </div>
+</div>
+{% endblock %}
diff --git a/rpki/gui/app/templates/app/routes_view.html b/rpki/gui/app/templates/app/routes_view.html
new file mode 100644
index 00000000..885f3fa9
--- /dev/null
+++ b/rpki/gui/app/templates/app/routes_view.html
@@ -0,0 +1,55 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+{% load bootstrap_pager %}
+{% load app_extras %}
+
+{% block sidebar_extra %}
+<li class="nav-header">BGP data updated</li>
+<li>IPv4: {{ timestamp.bgp_v4_import.isoformat }}</li>
+<li>IPv6: {{ timestamp.bgp_v6_import.isoformat }}</li>
+<li class="nav-header">rcynic cache updated</li>
+<li>{{ timestamp.rcynic_import.isoformat }}</li>
+{% endblock sidebar_extra %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Route View</h1>
+</div>
+
+<p>
+This view shows currently advertised routes for the prefixes listed in resource certs received from RPKI parents.
+
+<form method="POST" action="{% url "suggest-roas" %}">
+ {% csrf_token %}
+ <table class='table table-striped table-condensed'>
+ <thead>
+ <tr>
+ <th></th>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for r in routes %}
+ <tr>
+ <td><input type="checkbox" name="pk-{{ r.pk }}"></td>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td>
+ {% validity_label r.status %}
+ <a href='{% url "rpki.gui.app.views.route_detail" r.pk %}' title='display ROAs covering this prefix'><i class="icon-info-sign"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <div class="form-actions">
+ <button type="submit" title="create ROAs for selected routes"><i class="icon-plus-sign"></i> Create ROAs</button>
+ </div>
+</form>
+
+{% bootstrap_pager request routes %}
+
+{% endblock content %}
diff --git a/rpki/gui/app/templates/app/user_list.html b/rpki/gui/app/templates/app/user_list.html
new file mode 100644
index 00000000..1b419ded
--- /dev/null
+++ b/rpki/gui/app/templates/app/user_list.html
@@ -0,0 +1,37 @@
+{% extends "app/app_base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Users</h1>
+</div>
+
+<p>
+This page lists all user accounts in the web interface. Note that this is distinct from the
+<a href="{% url "rpki.gui.app.views.resource_holder_list" %}">list of resource holders</a>.
+</p>
+
+<table class='table table-striped'>
+ <thead>
+ <tr>
+ <th>Username</th>
+ <th>Email</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for user in object_list %}
+ <tr>
+ <td>{{ user.username }}</td>
+ <td>{{ user.email }}</td>
+ <td>
+ <a class='btn btn-small' href='{% url "rpki.gui.app.views.user_edit" user.pk %}' title="Edit"><i class="icon-edit"></i></a>
+ <a class='btn btn-small' href='{% url "rpki.gui.app.views.user_delete" user.pk %}' title="Delete"><i class="icon-trash"></i></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+<a class='btn' href="{% url "rpki.gui.app.views.user_create" %}" title="create a new locally hosted resource handle"><i class="icon-plus-sign"></i> Create</a>
+{% endblock content %}
diff --git a/rpki/gui/app/templates/base.html b/rpki/gui/app/templates/base.html
new file mode 100644
index 00000000..08d0c112
--- /dev/null
+++ b/rpki/gui/app/templates/base.html
@@ -0,0 +1,63 @@
+{% load url from future %}
+{% load app_extras %}
+
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta name='Content-Type' content='text/html; charset=UTF-8'>
+ <title>{% block title %}RPKI {% if request.session.handle %}: {{ request.session.handle }}{% endif %}{% endblock %}</title>
+ {% block head %}{% endblock %}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.min.css" media="screen">
+ <link rel="icon" href="{{ STATIC_URL }}img/sui-riu.ico" type="image/x-icon">
+ <link rel="shortcut icon" href="{{ STATIC_URL }}img/sui-riu.ico" type="image/x-icon">
+ <style type="text/css">
+ body { padding: 40px }
+ </style>
+ </head>
+ <body>
+
+ <!-- TOP BAR -->
+ <div class="container">
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <a class="brand" href="#">rpki.net {% rpki_version %}</a>
+ <ul class='nav'>
+ <li class="active">
+ <a href="#">Home</a>
+ </li>
+ <li class="divider-vertical"></li>
+ {% if user.is_authenticated %}
+ <li><p class="navbar-text">Logged in as {{ user }}</li>
+ <li class="divider-vertical"></li>
+ <li><a href="{% url "rpki.gui.views.logout" %}">Log Out</a></li>
+ {% endif %}
+ </ul>
+ <ul class="nav pull-right">
+ <li><a href="https://trac.rpki.net/wiki/doc/RPKI/CA/UI/GUI">Help</a></li>
+ </ul>
+ </div>
+ </div>
+ </div><!-- topbar -->
+
+ <div class="container-fluid">
+ <!-- MAIN CONTENT -->
+ <div class="row-fluid">
+ <div class="span2">
+ {% block sidebar %}{% endblock %}
+ </div>
+
+ <div class="span10">
+ {% if messages %}
+ {% for message in messages %}
+ {# this will break if there is more than one tag, but don't expect to use that feature #}
+ <div class='alert alert-{{ message.tags }}'>
+ {{ message }}
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+ </div>
+
+ </body>
+</html>
diff --git a/rpki/gui/app/templates/registration/login.html b/rpki/gui/app/templates/registration/login.html
new file mode 100644
index 00000000..0d6fb6fd
--- /dev/null
+++ b/rpki/gui/app/templates/registration/login.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% load url from future %}
+
+{% block content %}
+<div class="page-header">
+ <h1>Login</h1>
+</div>
+
+{% if form.errors %}
+<div class='alert'>
+ <p>Your username and password didn't match. Please try again.</p>
+</div>
+{% endif %}
+
+<form class="form-horizontal" method="post" action="{% url "rpki.gui.views.login" %}">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+
+ <input type="hidden" name="next" value="{{ next }}" />
+ <div class="form-actions">
+ <input type="submit" value="Login" class="btn btn-primary" />
+ </div>
+</form>
+
+{% endblock %}
diff --git a/rpki/gui/app/templatetags/__init__.py b/rpki/gui/app/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/app/templatetags/__init__.py
diff --git a/rpki/gui/app/templatetags/app_extras.py b/rpki/gui/app/templatetags/app_extras.py
new file mode 100644
index 00000000..2bde9bc2
--- /dev/null
+++ b/rpki/gui/app/templatetags/app_extras.py
@@ -0,0 +1,58 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def verbose_name(obj):
+ "Return the model class' verbose name."
+ return obj._meta.verbose_name.capitalize()
+
+
+@register.simple_tag
+def verbose_name_plural(qs):
+ "Return the verbose name for the model class."
+ return qs.model._meta.verbose_name_plural.capitalize()
+
+css = {
+ 'valid': 'label-success',
+ 'invalid': 'label-important'
+}
+
+
+@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)
+
+
+@register.simple_tag
+def rpki_version():
+ import rpki.version
+ return rpki.version.VERSION
diff --git a/rpki/gui/app/templatetags/bootstrap_pager.py b/rpki/gui/app/templatetags/bootstrap_pager.py
new file mode 100644
index 00000000..bae8445a
--- /dev/null
+++ b/rpki/gui/app/templatetags/bootstrap_pager.py
@@ -0,0 +1,55 @@
+from django import template
+
+register = template.Library()
+
+
+class BootstrapPagerNode(template.Node):
+ def __init__(self, request, pager_object):
+ self.request = template.Variable(request)
+ self.pager_object = template.Variable(pager_object)
+
+ def render(self, context):
+ request = self.request.resolve(context)
+ pager_object = self.pager_object.resolve(context)
+ if pager_object.paginator.num_pages == 1:
+ return ''
+ r = ['<div class="pagination"><ul>']
+ if pager_object.number == 1:
+ r.append('<li class="disabled"><a>&laquo;</a></li>')
+ else:
+ r.append('<li><a href="%s?page=%d">&laquo;</a></li>' % (request.path, pager_object.number - 1))
+
+ # display at most 5 pages around the current page
+ min_page = max(pager_object.number - 2, 1)
+ max_page = min(min_page + 5, pager_object.paginator.num_pages)
+
+ if min_page > 1:
+ r.append('<li><a href="%s">1</a></li>' % request.path)
+ r.append('<li class="disabled"><a>&hellip;</a></li>')
+
+ for i in range(min_page, max_page + 1):
+ r.append('<li %s><a href="%s?page=%d">%d</a></li>' % ('' if i != pager_object.number else 'class="active"', request.path, i, i))
+
+ if max_page < pager_object.paginator.num_pages:
+ r.append('<li class="disabled"><a>&hellip;</a></li>')
+ r.append('<li><a href="%(path)s?page=%(page)d">%(page)d</a></li>' %
+ {'path': request.path,
+ 'page': pager_object.paginator.num_pages})
+
+ if pager_object.number < pager_object.paginator.num_pages:
+ r.append('<li><a href="%s?page=%d">&raquo;</a></li>' % (request.path, pager_object.number + 1))
+ else:
+ r.append('<li class="disabled"><a>&raquo;</a></li>')
+
+
+ r.append('</ul></div>')
+ return '\n'.join(r)
+
+
+@register.tag
+def bootstrap_pager(parser, token):
+ try:
+ tag_name, request, pager_object = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError("%r tag requires two arguments" % token.contents.split()[0])
+ return BootstrapPagerNode(request, pager_object)
diff --git a/rpki/gui/app/timestamp.py b/rpki/gui/app/timestamp.py
new file mode 100644
index 00000000..959f2025
--- /dev/null
+++ b/rpki/gui/app/timestamp.py
@@ -0,0 +1,25 @@
+# $Id$
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+#
+
+import models
+from datetime import datetime
+
+def update(name):
+ "Set the timestamp value for the given name to the current time."
+ q = models.Timestamp.objects.filter(name=name)
+ obj = q[0] if q else models.Timestamp(name=name)
+ obj.ts = datetime.utcnow()
+ obj.save()
diff --git a/rpki/gui/app/urls.py b/rpki/gui/app/urls.py
new file mode 100644
index 00000000..92e90b0e
--- /dev/null
+++ b/rpki/gui/app/urls.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.conf.urls import patterns, url
+from rpki.gui.app import views
+
+urlpatterns = patterns(
+ '',
+ (r'^$', views.dashboard),
+ url(r'^alert/$', views.AlertListView.as_view(), name='alert-list'),
+ url(r'^alert/clear_all$', views.alert_clear_all, name='alert-clear-all'),
+ 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),
+ url(r'^conf/export_asns$', views.export_asns, name='export-asns'),
+ url(r'^conf/export_prefixes$', views.export_prefixes, name='export-prefixes'),
+ url(r'^conf/import_asns$', views.import_asns, name='import-asns'),
+ url(r'^conf/import_prefixes$', views.import_prefixes, name='import-prefixes'),
+ (r'^parent/import$', views.parent_import),
+ (r'^parent/(?P<pk>\d+)/$', views.parent_detail),
+ (r'^parent/(?P<pk>\d+)/delete$', views.parent_delete),
+ (r'^parent/(?P<pk>\d+)/export$', views.parent_export),
+ (r'^child/import$', views.child_import),
+ (r'^child/(?P<pk>\d+)/$', views.child_detail),
+ (r'^child/(?P<pk>\d+)/add_address$', views.child_add_prefix),
+ (r'^child/(?P<pk>\d+)/add_asn$', views.child_add_asn),
+ (r'^child/(?P<pk>\d+)/delete$', views.child_delete),
+ (r'^child/(?P<pk>\d+)/edit$', views.child_edit),
+ (r'^child/(?P<pk>\d+)/export$', views.child_response),
+ url(r'^gbr/create$', views.ghostbuster_create, name='gbr-create'),
+ url(r'^gbr/(?P<pk>\d+)/$', views.GhostbusterDetailView.as_view(), name='gbr-detail'),
+ url(r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit, name='gbr-edit'),
+ url(r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete, name='gbr-delete'),
+ (r'^refresh$', views.refresh),
+ (r'^client/import$', views.client_import),
+ (r'^client/$', views.client_list),
+ (r'^client/(?P<pk>\d+)/$', views.client_detail),
+ (r'^client/(?P<pk>\d+)/delete$', views.client_delete),
+ url(r'^client/(?P<pk>\d+)/export$', views.client_export, name='client-export'),
+ (r'^repo/import$', views.repository_import),
+ (r'^repo/(?P<pk>\d+)/$', views.repository_detail),
+ (r'^repo/(?P<pk>\d+)/delete$', views.repository_delete),
+ (r'^resource_holder/$', views.resource_holder_list),
+ (r'^resource_holder/create$', views.resource_holder_create),
+ (r'^resource_holder/(?P<pk>\d+)/delete$', views.resource_holder_delete),
+ (r'^resource_holder/(?P<pk>\d+)/edit$', views.resource_holder_edit),
+ (r'^roa/(?P<pk>\d+)/$', views.roa_detail),
+ (r'^roa/create$', views.roa_create),
+ (r'^roa/create_multi$', views.roa_create_multi),
+ (r'^roa/confirm$', views.roa_create_confirm),
+ (r'^roa/confirm_multi$', views.roa_create_multi_confirm),
+ url(r'^roa/export$', views.roa_export, name='roa-export'),
+ url(r'^roa/import$', views.roa_import, name='roa-import'),
+ (r'^roa/(?P<pk>\d+)/delete$', views.roa_delete),
+ url(r'^roa/(?P<pk>\d+)/clone$', views.roa_clone, name="roa-clone"),
+ (r'^route/$', views.route_view),
+ (r'^route/(?P<pk>\d+)/$', views.route_detail),
+ url(r'^route/suggest$', views.route_suggest, name="suggest-roas"),
+ (r'^user/$', views.user_list),
+ (r'^user/create$', views.user_create),
+ (r'^user/(?P<pk>\d+)/delete$', views.user_delete),
+ (r'^user/(?P<pk>\d+)/edit$', views.user_edit),
+)
diff --git a/rpki/gui/app/views.py b/rpki/gui/app/views.py
new file mode 100644
index 00000000..db4cf0c1
--- /dev/null
+++ b/rpki/gui/app/views.py
@@ -0,0 +1,1314 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+"""
+This module contains the view functions implementing the web portal
+interface.
+
+"""
+
+__version__ = '$Id$'
+
+import os
+import os.path
+from tempfile import NamedTemporaryFile
+import cStringIO
+import csv
+import logging
+
+from django.utils.decorators import method_decorator
+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, reverse_lazy
+from django.contrib.auth.models import User
+from django.views.generic import DetailView, ListView, DeleteView
+from django.core.paginator import Paginator, InvalidPage
+from django.forms.formsets import formset_factory, BaseFormSet
+import django.db.models
+from django.contrib import messages
+
+from rpki.irdb import Zookeeper, ChildASN, ChildNet, ROARequestPrefix
+from rpki.gui.app import models, forms, glue, range_list
+from rpki.resource_set import (resource_range_as, resource_range_ip,
+ roa_prefix_ipv4)
+from rpki import sundial
+import rpki.exceptions
+
+from rpki.gui.cacheview.models import ROA
+from rpki.gui.routeview.models import RouteOrigin
+from rpki.gui.decorators import tls_required
+
+logger = logging.getLogger(__name__)
+
+
+def superuser_required(f):
+ """Decorator which returns HttpResponseForbidden if the user does
+ not have superuser permissions.
+
+ """
+ @login_required
+ def _wrapped(request, *args, **kwargs):
+ if not request.user.is_superuser:
+ return http.HttpResponseForbidden()
+ return f(request, *args, **kwargs)
+ return _wrapped
+
+
+def handle_required(f):
+ """Decorator for view functions which require the user to be logged in and
+ a resource handle selected for the session.
+
+ """
+ @login_required
+ @tls_required
+ def wrapped_fn(request, *args, **kwargs):
+ if 'handle' not in request.session:
+ if request.user.is_superuser:
+ conf = models.Conf.objects.all()
+ else:
+ conf = models.Conf.objects.filter(confacl__user=request.user)
+
+ if conf.count() == 1:
+ request.session['handle'] = conf[0]
+ elif conf.count() == 0:
+ return render(request, 'app/conf_empty.html', {})
+ else:
+ url = '%s?next=%s' % (reverse(conf_list),
+ urlquote(request.get_full_path()))
+ return http.HttpResponseRedirect(url)
+
+ return f(request, *args, **kwargs)
+ return wrapped_fn
+
+
+@handle_required
+def generic_import(request, queryset, configure, form_class=None,
+ post_import_redirect=None):
+ """
+ Generic view function for importing XML files used in the setup
+ process.
+
+ queryset
+ queryset containing all objects of the type being imported
+
+ configure
+ method on Zookeeper to invoke with the imported XML file
+
+ form_class
+ specifies the form to use for import. If None, uses the generic
+ forms.ImportForm.
+
+ post_import_redirect
+ if None (default), the user will be redirected to the detail page for
+ the imported object. Otherwise, the user will be redirected to the
+ specified URL.
+
+ """
+ conf = request.session['handle']
+ if form_class is None:
+ form_class = forms.ImportForm
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ tmpf = NamedTemporaryFile(prefix='import', suffix='.xml',
+ delete=False)
+ tmpf.write(form.cleaned_data['xml'].read())
+ tmpf.close()
+ z = Zookeeper(handle=conf.handle)
+ handle = form.cleaned_data.get('handle')
+ # CharField uses an empty string for the empty value, rather than
+ # None. Convert to none in this case, since configure_child/parent
+ # expects it.
+ if handle == '':
+ handle = None
+ # configure_repository returns None, so can't use tuple expansion
+ # here. Unpack the tuple below if post_import_redirect is None.
+ r = configure(z, tmpf.name, handle)
+ # force rpkid run now
+ z.synchronize_ca(poke=True)
+ os.remove(tmpf.name)
+ if post_import_redirect:
+ url = post_import_redirect
+ else:
+ _, handle = r
+ url = queryset.get(issuer=conf,
+ handle=handle).get_absolute_url()
+ return http.HttpResponseRedirect(url)
+ else:
+ form = form_class()
+
+ return render(request, 'app/app_form.html', {
+ 'form': form,
+ 'form_title': 'Import ' + queryset.model._meta.verbose_name.capitalize(),
+ })
+
+
+@handle_required
+def dashboard(request):
+ conf = request.session['handle']
+
+ used_asns = range_list.RangeList()
+
+ # asns used in my roas
+ qs = models.ROARequest.objects.filter(issuer=conf)
+ roa_asns = set((obj.asn for obj in qs))
+ used_asns.extend((resource_range_as(asn, asn) for asn in roa_asns))
+
+ # asns given to my children
+ child_asns = ChildASN.objects.filter(child__in=conf.children.all())
+ used_asns.extend((resource_range_as(obj.start_as, obj.end_as) for obj in child_asns))
+
+ # my received asns
+ asns = models.ResourceRangeAS.objects.filter(cert__conf=conf)
+ my_asns = range_list.RangeList([resource_range_as(obj.min, obj.max) for obj in asns])
+
+ unused_asns = my_asns.difference(used_asns)
+
+ used_prefixes = range_list.RangeList()
+ used_prefixes_v6 = range_list.RangeList()
+
+ # prefixes used in my roas
+ for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf,
+ version='IPv4'):
+ used_prefixes.append(obj.as_resource_range())
+
+ for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf,
+ version='IPv6'):
+ used_prefixes_v6.append(obj.as_resource_range())
+
+ # prefixes given to my children
+ for obj in ChildNet.objects.filter(child__in=conf.children.all(),
+ version='IPv4'):
+ used_prefixes.append(obj.as_resource_range())
+
+ for obj in ChildNet.objects.filter(child__in=conf.children.all(),
+ version='IPv6'):
+ used_prefixes_v6.append(obj.as_resource_range())
+
+ # my received prefixes
+ prefixes = models.ResourceRangeAddressV4.objects.filter(cert__conf=conf).all()
+ prefixes_v6 = models.ResourceRangeAddressV6.objects.filter(cert__conf=conf).all()
+ my_prefixes = range_list.RangeList([obj.as_resource_range() for obj in prefixes])
+ my_prefixes_v6 = range_list.RangeList([obj.as_resource_range() for obj in prefixes_v6])
+
+ unused_prefixes = my_prefixes.difference(used_prefixes)
+ # monkey-patch each object with a boolean value indicating whether or not
+ # it is a prefix. We have to do this here because in the template there is
+ # no way to catch the MustBePrefix exception.
+ for x in unused_prefixes:
+ try:
+ x.prefixlen()
+ x.is_prefix = True
+ except rpki.exceptions.MustBePrefix:
+ x.is_prefix = False
+
+ unused_prefixes_v6 = my_prefixes_v6.difference(used_prefixes_v6)
+ for x in unused_prefixes_v6:
+ try:
+ x.prefixlen()
+ x.is_prefix = True
+ except rpki.exceptions.MustBePrefix:
+ x.is_prefix = False
+
+ clients = models.Client.objects.all() if request.user.is_superuser else None
+
+ return render(request, 'app/dashboard.html', {
+ 'conf': conf,
+ 'unused_asns': unused_asns,
+ 'unused_prefixes': unused_prefixes,
+ 'unused_prefixes_v6': unused_prefixes_v6,
+ 'asns': asns,
+ 'prefixes': prefixes,
+ 'prefixes_v6': prefixes_v6,
+ 'clients': clients,
+ })
+
+
+@login_required
+def conf_list(request, **kwargs):
+ """Allow the user to select a handle."""
+ log = request.META['wsgi.errors']
+ next_url = request.GET.get('next', reverse(dashboard))
+ if request.user.is_superuser:
+ qs = models.Conf.objects.all()
+ else:
+ qs = models.Conf.objects.filter(confacl__user=request.user)
+ return render(request, 'app/conf_list.html', {
+ 'conf_list': qs,
+ 'next_url': next_url
+ })
+
+
+@login_required
+def conf_select(request):
+ """Change the handle for the current session."""
+ if not 'handle' in request.GET:
+ return redirect(conf_list)
+ handle = request.GET['handle']
+ next_url = request.GET.get('next', reverse(dashboard))
+ if request.user.is_superuser:
+ request.session['handle'] = get_object_or_404(models.Conf, handle=handle)
+ else:
+ request.session['handle'] = get_object_or_404(
+ models.Conf, confacl__user=request.user, handle=handle
+ )
+ return http.HttpResponseRedirect(next_url)
+
+
+def serve_xml(content, basename, ext='xml'):
+ """
+ Generate a HttpResponse object with the content type set to XML.
+
+ `content` is a string.
+
+ `basename` is the prefix to specify for the XML filename.
+
+ `csv` is the type (default: xml)
+
+ """
+ resp = http.HttpResponse(content, mimetype='application/%s' % ext)
+ resp['Content-Disposition'] = 'attachment; filename=%s.%s' % (basename, ext)
+ return resp
+
+
+@handle_required
+def conf_export(request):
+ """Return the identity.xml for the current handle."""
+ conf = request.session['handle']
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_identity()
+ return serve_xml(str(xml), '%s.identity' % conf.handle)
+
+
+@handle_required
+def export_asns(request):
+ """Export CSV file containing ASN allocations to children."""
+ conf = request.session['handle']
+ s = cStringIO.StringIO()
+ csv_writer = csv.writer(s, delimiter=' ')
+ for childasn in ChildASN.objects.filter(child__issuer=conf):
+ csv_writer.writerow([childasn.child.handle, str(childasn.as_resource_range())])
+ return serve_xml(s.getvalue(), '%s.asns' % conf.handle, ext='csv')
+
+
+@handle_required
+def import_asns(request):
+ conf = request.session['handle']
+ if request.method == 'POST':
+ form = forms.ImportCSVForm(request.POST, request.FILES)
+ if form.is_valid():
+ f = NamedTemporaryFile(prefix='asns', suffix='.csv', delete=False)
+ f.write(request.FILES['csv'].read())
+ f.close()
+ z = Zookeeper(handle=conf.handle)
+ z.load_asns(f.name)
+ z.run_rpkid_now()
+ os.unlink(f.name)
+ messages.success(request, 'Successfully imported AS delgations from CSV file.')
+ return redirect(dashboard)
+ else:
+ form = forms.ImportCSVForm()
+ return render(request, 'app/import_resource_form.html', {
+ 'form_title': 'Import CSV containing ASN delegations',
+ 'form': form,
+ 'cancel_url': reverse(dashboard)
+ })
+
+
+@handle_required
+def export_prefixes(request):
+ """Export CSV file containing ASN allocations to children."""
+ conf = request.session['handle']
+ s = cStringIO.StringIO()
+ csv_writer = csv.writer(s, delimiter=' ')
+ for childnet in ChildNet.objects.filter(child__issuer=conf):
+ csv_writer.writerow([childnet.child.handle, str(childnet.as_resource_range())])
+ return serve_xml(s.getvalue(), '%s.prefixes' % conf.handle, ext='csv')
+
+
+@handle_required
+def import_prefixes(request):
+ conf = request.session['handle']
+ if request.method == 'POST':
+ form = forms.ImportCSVForm(request.POST, request.FILES)
+ if form.is_valid():
+ f = NamedTemporaryFile(prefix='prefixes', suffix='.csv', delete=False)
+ f.write(request.FILES['csv'].read())
+ f.close()
+ z = Zookeeper(handle=conf.handle)
+ z.load_prefixes(f.name)
+ z.run_rpkid_now()
+ os.unlink(f.name)
+ messages.success(request, 'Successfully imported prefix delegations from CSV file.')
+ return redirect(dashboard)
+ else:
+ form = forms.ImportCSVForm()
+ return render(request, 'app/import_resource_form.html', {
+ 'form_title': 'Import CSV containing Prefix delegations',
+ 'form': form,
+ 'cancel_url': reverse(dashboard)
+ })
+
+
+@handle_required
+def parent_import(request):
+ conf = request.session['handle']
+ return generic_import(request, conf.parents, Zookeeper.configure_parent)
+
+
+@handle_required
+def parent_detail(request, pk):
+ return render(request, 'app/parent_detail.html', {
+ 'object': get_object_or_404(request.session['handle'].parents, pk=pk)})
+
+
+@handle_required
+def parent_delete(request, pk):
+ conf = request.session['handle']
+ obj = get_object_or_404(conf.parents, pk=pk) # confirm permission
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_parent(obj.handle)
+ z.synchronize_ca()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.Empty()
+ return render(request, 'app/object_confirm_delete.html', {
+ 'object': obj,
+ 'form': form,
+ 'parent_template': 'app/parent_detail.html'
+ })
+
+
+@handle_required
+def parent_export(request, pk):
+ """Export XML repository request for a given parent."""
+ conf = request.session['handle']
+ parent = get_object_or_404(conf.parents, pk=pk)
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_repository_request(parent)
+ return serve_xml(str(xml), '%s.repository' % parent.handle)
+
+
+@handle_required
+def child_import(request):
+ conf = request.session['handle']
+ return generic_import(request, conf.children, Zookeeper.configure_child)
+
+
+@handle_required
+def child_add_prefix(request, pk):
+ logstream = request.META['wsgi.errors']
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children, pk=pk)
+ if request.method == 'POST':
+ form = forms.AddNetForm(request.POST, child=child)
+ if form.is_valid():
+ address_range = form.cleaned_data.get('address_range')
+ r = resource_range_ip.parse_str(address_range)
+ version = 'IPv%d' % r.version
+ child.address_ranges.create(start_ip=str(r.min), end_ip=str(r.max),
+ version=version)
+ Zookeeper(handle=conf.handle, logstream=logstream).run_rpkid_now()
+ return http.HttpResponseRedirect(child.get_absolute_url())
+ else:
+ form = forms.AddNetForm(child=child)
+ return render(request, 'app/app_form.html',
+ {'object': child, 'form': form, 'form_title': 'Add Prefix'})
+
+
+@handle_required
+def child_add_asn(request, pk):
+ logstream = request.META['wsgi.errors']
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children, pk=pk)
+ if request.method == 'POST':
+ form = forms.AddASNForm(request.POST, child=child)
+ if form.is_valid():
+ asns = form.cleaned_data.get('asns')
+ r = resource_range_as.parse_str(asns)
+ child.asns.create(start_as=r.min, end_as=r.max)
+ Zookeeper(handle=conf.handle, logstream=logstream).run_rpkid_now()
+ return http.HttpResponseRedirect(child.get_absolute_url())
+ else:
+ form = forms.AddASNForm(child=child)
+ return render(request, 'app/app_form.html',
+ {'object': child, 'form': form, 'form_title': 'Add ASN'})
+
+
+@handle_required
+def child_detail(request, pk):
+ child = get_object_or_404(request.session['handle'].children, pk=pk)
+ return render(request, 'app/child_detail.html', {'object': child})
+
+
+@handle_required
+def child_edit(request, pk):
+ """Edit the end validity date for a resource handle's child."""
+ log = request.META['wsgi.errors']
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children.all(), pk=pk)
+ form_class = forms.ChildForm(child)
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ child.valid_until = sundial.datetime.from_datetime(form.cleaned_data.get('valid_until'))
+ child.save()
+ # remove AS & prefixes that are not selected in the form
+ models.ChildASN.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('as_ranges')).delete()
+ models.ChildNet.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('address_ranges')).delete()
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(child.get_absolute_url())
+ else:
+ form = form_class(initial={
+ 'as_ranges': child.asns.all(),
+ 'address_ranges': child.address_ranges.all()})
+
+ return render(request, 'app/app_form.html', {
+ 'object': child,
+ 'form': form,
+ 'form_title': 'Edit Child: ' + child.handle,
+ })
+
+
+@handle_required
+def child_response(request, pk):
+ """
+ Export the XML file containing the output of the configure_child
+ to send back to the client.
+
+ """
+ conf = request.session['handle']
+ child = get_object_or_404(models.Child, issuer=conf, pk=pk)
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_parental_response(child)
+ resp = serve_xml(str(xml), child.handle)
+ return resp
+
+
+@handle_required
+def child_delete(request, pk):
+ logstream = request.META['wsgi.errors']
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children, pk=pk)
+ if request.method == 'POST':
+ form = forms.Empty(request.POST)
+ if form.is_valid():
+ z = Zookeeper(handle=conf.handle, logstream=logstream)
+ z.delete_child(child.handle)
+ z.synchronize_ca()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.Empty()
+ return render(request, 'app/object_confirm_delete.html', {
+ 'object': child,
+ 'form': form,
+ 'parent_template': 'app/child_detail.html'
+ })
+
+
+@handle_required
+def roa_detail(request, pk):
+ conf = request.session['handle']
+ obj = get_object_or_404(conf.roas, pk=pk)
+ return render(request, 'app/roa_detail.html', {'object': obj})
+
+
+def get_covered_routes(rng, max_prefixlen, asn):
+ """Returns a list of routeview.models.RouteOrigin objects which would
+ change validation status if a ROA were created with the parameters to this
+ function.
+
+ A "newstatus" attribute is monkey-patched on the RouteOrigin objects which
+ can be used in the template. "status" remains the current validation
+ status of the object.
+
+ """
+
+ # find all routes that match or are completed covered by the proposed new roa
+ qs = RouteOrigin.objects.filter(
+ prefix_min__gte=rng.min,
+ prefix_max__lte=rng.max
+ )
+ routes = []
+ for route in qs:
+ status = route.status
+ # tweak the validation status due to the presence of the
+ # new ROA. Don't need to check the prefix bounds here
+ # because all the matches routes will be covered by this
+ # new ROA
+ if status == 'unknown':
+ # if the route was previously unknown (no covering
+ # ROAs), then:
+ # if the AS matches, it is valid, otherwise invalid
+ if (route.asn != 0 and route.asn == asn and route.prefixlen <= max_prefixlen):
+ route.newstatus = 'valid'
+ else:
+ route.newstatus = 'invalid'
+ routes.append(route)
+ elif status == 'invalid':
+ # if the route was previously invalid, but this new ROA
+ # matches the ASN, it is now valid
+ if route.asn != 0 and route.asn == asn and route.prefixlen <= max_prefixlen:
+ route.newstatus = 'valid'
+ routes.append(route)
+
+ return routes
+
+
+@handle_required
+def roa_create(request):
+ """Present the user with a form to create a ROA.
+
+ Doesn't use the generic create_object() form because we need to
+ create both the ROARequest and ROARequestPrefix objects.
+
+ """
+
+ conf = request.session['handle']
+ if request.method == 'POST':
+ form = forms.ROARequest(request.POST, request.FILES, conf=conf)
+ if form.is_valid():
+ asn = form.cleaned_data.get('asn')
+ rng = form._as_resource_range() # FIXME calling "private" method
+ max_prefixlen = int(form.cleaned_data.get('max_prefixlen'))
+
+ routes = get_covered_routes(rng, max_prefixlen, asn)
+
+ prefix = str(rng)
+ form = forms.ROARequestConfirm(initial={'asn': asn,
+ 'prefix': prefix,
+ 'max_prefixlen': max_prefixlen})
+ return render(request, 'app/roarequest_confirm_form.html',
+ {'form': form,
+ 'asn': asn,
+ 'prefix': prefix,
+ 'max_prefixlen': max_prefixlen,
+ 'routes': routes})
+ else:
+ # pull initial values from query parameters
+ d = {}
+ for s in ('asn', 'prefix'):
+ if s in request.GET:
+ d[s] = request.GET[s]
+ form = forms.ROARequest(initial=d)
+
+ return render(request, 'app/roarequest_form.html', {'form': form})
+
+
+class ROARequestFormSet(BaseFormSet):
+ """There is no way to pass arbitrary keyword arguments to the form
+ constructor, so we have to override BaseFormSet to allow it.
+
+ """
+ def __init__(self, *args, **kwargs):
+ self.conf = kwargs.pop('conf')
+ super(ROARequestFormSet, self).__init__(*args, **kwargs)
+
+ def _construct_forms(self):
+ self.forms = []
+ for i in xrange(self.total_form_count()):
+ self.forms.append(self._construct_form(i, conf=self.conf))
+
+
+def split_with_default(s):
+ xs = s.split(',')
+ if len(xs) == 1:
+ return xs[0], None
+ return xs
+
+
+@handle_required
+def roa_create_multi(request):
+ """version of roa_create that uses a formset to allow entry of multiple
+ roas on a single page.
+
+ ROAs can be specified in the GET query string, as such:
+
+ ?roa=prefix,asn
+
+ Mulitple ROAs may be specified:
+
+ ?roa=prefix,asn+roa=prefix2,asn2
+
+ If an IP range is specified, it will be automatically split into multiple
+ prefixes:
+
+ ?roa=1.1.1.1-2.2.2.2,42
+
+ The ASN may optionally be omitted.
+
+ """
+
+ conf = request.session['handle']
+ if request.method == 'GET':
+ init = []
+ for x in request.GET.getlist('roa'):
+ rng, asn = split_with_default(x)
+ rng = resource_range_ip.parse_str(rng)
+ if rng.can_be_prefix:
+ init.append({'asn': asn, 'prefix': str(rng)})
+ else:
+ v = []
+ rng.chop_into_prefixes(v)
+ init.extend([{'asn': asn, 'prefix': str(p)} for p in v])
+ formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
+ can_delete=True)(initial=init, conf=conf)
+ elif request.method == 'POST':
+ formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
+ extra=0, can_delete=True)(request.POST, request.FILES, conf=conf)
+ if formset.is_valid():
+ routes = []
+ v = []
+ # as of Django 1.4.5 we still can't use formset.cleaned_data
+ # because deleted forms are not excluded, which causes an
+ # AttributeError to be raised.
+ for form in formset:
+ if hasattr(form, 'cleaned_data') and form.cleaned_data: # exclude empty forms
+ asn = form.cleaned_data.get('asn')
+ rng = resource_range_ip.parse_str(form.cleaned_data.get('prefix'))
+ max_prefixlen = int(form.cleaned_data.get('max_prefixlen'))
+ # FIXME: This won't do the right thing in the event that a
+ # route is covered by multiple ROAs created in the form.
+ # You will see duplicate entries, each with a potentially
+ # different validation status.
+ routes.extend(get_covered_routes(rng, max_prefixlen, asn))
+ v.append({'prefix': str(rng), 'max_prefixlen': max_prefixlen,
+ 'asn': asn})
+ # if there were no rows, skip the confirmation step
+ if v:
+ formset = formset_factory(forms.ROARequestConfirm, extra=0)(initial=v)
+ return render(request, 'app/roarequest_confirm_multi_form.html',
+ {'routes': routes, 'formset': formset, 'roas': v})
+ return render(request, 'app/roarequest_multi_form.html',
+ {'formset': formset})
+
+
+@handle_required
+def roa_create_confirm(request):
+ """This function is called when the user confirms the creation of a ROA
+ request. It is responsible for updating the IRDB.
+
+ """
+ conf = request.session['handle']
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.ROARequestConfirm(request.POST, request.FILES)
+ if form.is_valid():
+ asn = form.cleaned_data.get('asn')
+ prefix = form.cleaned_data.get('prefix')
+ rng = resource_range_ip.parse_str(prefix)
+ max_prefixlen = form.cleaned_data.get('max_prefixlen')
+ # Always create ROA requests with a single prefix.
+ # https://trac.rpki.net/ticket/32
+ roa = models.ROARequest.objects.create(issuer=conf, asn=asn)
+ v = 'IPv%d' % rng.version
+ roa.prefixes.create(version=v, prefix=str(rng.min),
+ prefixlen=rng.prefixlen(),
+ max_prefixlen=max_prefixlen)
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ # What should happen when the submission form isn't valid? For now
+ # just fall through and redirect back to the ROA creation form
+ return http.HttpResponseRedirect(reverse(roa_create))
+
+
+@handle_required
+def roa_create_multi_confirm(request):
+ """This function is called when the user confirms the creation of a ROA
+ request. It is responsible for updating the IRDB.
+
+ """
+ conf = request.session['handle']
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ formset = formset_factory(forms.ROARequestConfirm, extra=0)(request.POST, request.FILES)
+ if formset.is_valid():
+ for cleaned_data in formset.cleaned_data:
+ asn = cleaned_data.get('asn')
+ prefix = cleaned_data.get('prefix')
+ rng = resource_range_ip.parse_str(prefix)
+ max_prefixlen = cleaned_data.get('max_prefixlen')
+ # Always create ROA requests with a single prefix.
+ # https://trac.rpki.net/ticket/32
+ roa = models.ROARequest.objects.create(issuer=conf, asn=asn)
+ v = 'IPv%d' % rng.version
+ roa.prefixes.create(version=v, prefix=str(rng.min),
+ prefixlen=rng.prefixlen(),
+ max_prefixlen=max_prefixlen)
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return redirect(dashboard)
+ # What should happen when the submission form isn't valid? For now
+ # just fall through and redirect back to the ROA creation form
+ return http.HttpResponseRedirect(reverse(roa_create_multi))
+
+
+@handle_required
+def roa_delete(request, pk):
+ """Handles deletion of a single ROARequest object.
+
+ Uses a form for double confirmation, displaying how the route
+ validation status may change as a result.
+
+ """
+
+ conf = request.session['handle']
+ roa = get_object_or_404(conf.roas, pk=pk)
+ if request.method == 'POST':
+ roa.delete()
+ Zookeeper(handle=conf.handle).run_rpkid_now()
+ return redirect(reverse(dashboard))
+
+ ### Process GET ###
+
+ # note: assumes we only generate one prefix per ROA
+ roa_prefix = roa.prefixes.all()[0]
+ rng = roa_prefix.as_resource_range()
+
+ routes = []
+ for route in roa.routes:
+ # select all roas which cover this route
+ # excluding the current roa
+ # note: we can't identify the exact ROA here, because we only know what
+ # was requested to rpkid
+ roas = route.roas.exclude(
+ asid=roa.asn,
+ prefixes__prefix_min=rng.min,
+ prefixes__prefix_max=rng.max,
+ prefixes__max_length=roa_prefix.max_prefixlen
+ )
+
+ # subselect exact match
+ if route.asn != 0 and roas.filter(asid=route.asn,
+ prefixes__max_length__gte=route.prefixlen).exists():
+ route.newstatus = 'valid'
+ elif roas.exists():
+ route.newstatus = 'invalid'
+ else:
+ route.newstatus = 'unknown'
+ # we may want to ignore routes for which there is no status change,
+ # but the user may want to see that nothing has changed explicitly
+ routes.append(route)
+
+ return render(request, 'app/roarequest_confirm_delete.html',
+ {'object': roa, 'routes': routes})
+
+
+@handle_required
+def roa_clone(request, pk):
+ conf = request.session['handle']
+ roa = get_object_or_404(conf.roas, pk=pk)
+ return redirect(
+ reverse(roa_create_multi) + "?roa=" + str(roa.prefixes.all()[0].as_roa_prefix())
+ )
+
+
+@handle_required
+def roa_import(request):
+ """Import CSV containing ROA declarations."""
+ if request.method == 'POST':
+ form = forms.ImportCSVForm(request.POST, request.FILES)
+ if form.is_valid():
+ import tempfile
+ tmp = tempfile.NamedTemporaryFile(suffix='.csv', prefix='roas', delete=False)
+ tmp.write(request.FILES['csv'].read())
+ tmp.close()
+ z = Zookeeper(handle=request.session['handle'])
+ z.load_roa_requests(tmp.name)
+ z.run_rpkid_now()
+ os.unlink(tmp.name)
+ messages.success(request, 'Successfully imported ROAs.')
+ return redirect(dashboard)
+ else:
+ form = forms.ImportCSVForm()
+ return render(request, 'app/import_resource_form.html', {
+ 'form_title': 'Import ROAs from CSV',
+ 'form': form,
+ 'cancel_url': reverse(dashboard)
+ })
+
+
+@handle_required
+def roa_export(request):
+ """Export CSV containing ROA declarations."""
+ # FIXME: remove when Zookeeper can do this
+ f = cStringIO.StringIO()
+ csv_writer = csv.writer(f, delimiter=' ')
+ conf = request.session['handle']
+ # each roa prefix gets a unique group so rpkid will issue separate roas
+ for group, roapfx in enumerate(ROARequestPrefix.objects.filter(roa_request__issuer=conf)):
+ csv_writer.writerow([str(roapfx.as_roa_prefix()), roapfx.roa_request.asn, '%s-%d' % (conf.handle, group)])
+ resp = http.HttpResponse(f.getvalue(), mimetype='application/csv')
+ resp['Content-Disposition'] = 'attachment; filename=roas.csv'
+ return resp
+
+
+class GhostbusterDetailView(DetailView):
+ def get_queryset(self):
+ return self.request.session['handle'].ghostbusters
+
+
+@handle_required
+def ghostbuster_delete(request, pk):
+ conf = request.session['handle']
+ logstream = request.META['wsgi.errors']
+ obj = get_object_or_404(conf.ghostbusters, pk=pk)
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ obj.delete()
+ Zookeeper(handle=conf.handle, logstream=logstream).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.Empty(request.POST, request.FILES)
+ return render(request, 'app/object_confirm_delete.html', {
+ 'object': obj,
+ 'form': form,
+ 'parent_template': 'app/ghostbusterrequest_detail.html'
+ })
+
+
+@handle_required
+def ghostbuster_create(request):
+ conf = request.session['handle']
+ logstream = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.GhostbusterRequestForm(request.POST, request.FILES,
+ conf=conf)
+ if form.is_valid():
+ obj = form.save(commit=False)
+ obj.vcard = glue.ghostbuster_to_vcard(obj)
+ obj.save()
+ Zookeeper(handle=conf.handle, logstream=logstream).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.GhostbusterRequestForm(conf=conf)
+ return render(request, 'app/app_form.html',
+ {'form': form, 'form_title': 'New Ghostbuster Request'})
+
+
+@handle_required
+def ghostbuster_edit(request, pk):
+ conf = request.session['handle']
+ obj = get_object_or_404(conf.ghostbusters, pk=pk)
+ logstream = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.GhostbusterRequestForm(request.POST, request.FILES,
+ conf=conf, instance=obj)
+ if form.is_valid():
+ obj = form.save(commit=False)
+ obj.vcard = glue.ghostbuster_to_vcard(obj)
+ obj.save()
+ Zookeeper(handle=conf.handle, logstream=logstream).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.GhostbusterRequestForm(conf=conf, instance=obj)
+ return render(request, 'app/app_form.html',
+ {'form': form, 'form_title': 'Edit Ghostbuster Request'})
+
+
+@handle_required
+def refresh(request):
+ """
+ Query rpkid, update the db, and redirect back to the dashboard.
+
+ """
+ glue.list_received_resources(request.META['wsgi.errors'],
+ request.session['handle'])
+ return http.HttpResponseRedirect(reverse(dashboard))
+
+
+@handle_required
+def route_view(request):
+ """
+ Display a list of global routing table entries which match resources
+ listed in received certificates.
+
+ """
+ conf = request.session['handle']
+ count = request.GET.get('count', 25)
+ page = request.GET.get('page', 1)
+
+ paginator = Paginator(conf.routes, count)
+ try:
+ routes = paginator.page(page)
+ except InvalidPage:
+ # page was empty, or page number was invalid
+ routes = []
+ ts = dict((attr['name'], attr['ts']) for attr in models.Timestamp.objects.values())
+ return render(request, 'app/routes_view.html',
+ {'routes': routes, 'timestamp': ts})
+
+
+def route_detail(request, pk):
+ """Show a list of ROAs that match a given IPv4 route."""
+ route = get_object_or_404(models.RouteOrigin, pk=pk)
+ # when running rootd, viewing the 0.0.0.0/0 route will cause a fetch of all
+ # roas, so we paginate here, even though in the general case the number of
+ # objects will be small enough to fit a single page
+ count = request.GET.get('count', 25)
+ page = request.GET.get('page', 1)
+ paginator = Paginator(route.roa_prefixes.all(), count)
+ return render(request, 'app/route_detail.html', {
+ 'object': route,
+ 'roa_prefixes': paginator.page(page),
+ })
+
+
+def route_suggest(request):
+ """Handles POSTs from the route view and redirects to the ROA creation
+ page based on selected route objects. The form should contain elements of
+ the form "pk-NUM" where NUM is the RouteOrigin object id.
+
+ """
+ if request.method == 'POST':
+ routes = []
+ for pk in request.POST.iterkeys():
+ logger.debug(pk)
+ if pk.startswith("pk-"):
+ n = int(pk[3:])
+ routes.append(n)
+ qs = RouteOrigin.objects.filter(pk__in=routes)
+ s = []
+ for r in qs:
+ s.append('roa=%s/%d,%d' % (str(r.prefix_min), r.prefixlen, r.asn))
+ p = '&'.join(s)
+ return redirect(reverse(roa_create_multi) + '?' + p)
+
+
+@handle_required
+def repository_detail(request, pk):
+ conf = request.session['handle']
+ return render(request,
+ 'app/repository_detail.html',
+ {'object': get_object_or_404(conf.repositories, pk=pk)})
+
+
+@handle_required
+def repository_delete(request, pk):
+ log = request.META['wsgi.errors']
+ conf = request.session['handle']
+ # Ensure the repository being deleted belongs to the current user.
+ obj = get_object_or_404(models.Repository, issuer=conf, pk=pk)
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_repository(obj.handle)
+ z.synchronize_ca()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.Empty()
+ return render(request, 'app/object_confirm_delete.html', {
+ 'object': obj,
+ 'form': form,
+ 'parent_template':
+ 'app/repository_detail.html',
+ })
+
+
+@handle_required
+def repository_import(request):
+ """Import XML response file from repository operator."""
+ return generic_import(request,
+ models.Repository.objects,
+ Zookeeper.configure_repository,
+ form_class=forms.ImportRepositoryForm,
+ post_import_redirect=reverse(dashboard))
+
+
+@superuser_required
+def client_list(request):
+ """display a list of all repository client (irdb.models.Client)"""
+
+ return render(request, 'app/client_list.html', {
+ 'object_list': models.Client.objects.all()
+ })
+
+
+@superuser_required
+def client_detail(request, pk):
+ return render(request, 'app/client_detail.html',
+ {'object': get_object_or_404(models.Client, pk=pk)})
+
+
+@superuser_required
+def client_delete(request, pk):
+ log = request.META['wsgi.errors']
+ obj = get_object_or_404(models.Client, pk=pk)
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ z = Zookeeper(logstream=log)
+ z.delete_publication_client(obj.handle)
+ z.synchronize_pubd()
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ form = forms.Empty()
+ return render(request, 'app/object_confirm_delete.html', {
+ 'object': obj,
+ 'form': form,
+ 'parent_template': 'app/client_detail.html'
+ })
+
+
+@superuser_required
+def client_import(request):
+ return generic_import(request, models.Client.objects,
+ Zookeeper.configure_publication_client,
+ form_class=forms.ImportClientForm,
+ post_import_redirect=reverse(dashboard))
+
+
+@superuser_required
+def client_export(request, pk):
+ """Return the XML file resulting from a configure_publication_client
+ request.
+
+ """
+ client = get_object_or_404(models.Client, pk=pk)
+ z = Zookeeper()
+ xml = z.generate_repository_response(client)
+ return serve_xml(str(xml), '%s.repo' % z.handle)
+
+
+### Routines for managing resource handles serviced by this server
+
+@superuser_required
+def resource_holder_list(request):
+ """Display a list of all the RPKI handles managed by this server."""
+ return render(request, 'app/resource_holder_list.html', {
+ 'object_list': models.Conf.objects.all()
+ })
+
+
+@superuser_required
+def resource_holder_edit(request, pk):
+ """Display a list of all the RPKI handles managed by this server."""
+ conf = get_object_or_404(models.Conf, pk=pk)
+ if request.method == 'POST':
+ form = forms.ResourceHolderForm(request.POST, request.FILES)
+ if form.is_valid():
+ models.ConfACL.objects.filter(conf=conf).delete()
+ for user in form.cleaned_data.get('users'):
+ models.ConfACL.objects.create(user=user, conf=conf)
+ return redirect(resource_holder_list)
+ else:
+ users = [acl.user for acl in models.ConfACL.objects.filter(conf=conf).all()]
+ form = forms.ResourceHolderForm(initial={
+ 'users': users
+ })
+ return render(request, 'app/app_form.html', {
+ 'form_title': "Edit Resource Holder: " + conf.handle,
+ 'form': form,
+ 'cancel_url': reverse(resource_holder_list)
+ })
+
+
+@superuser_required
+def resource_holder_delete(request, pk):
+ conf = get_object_or_404(models.Conf, pk=pk)
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.Empty(request.POST)
+ if form.is_valid():
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_self()
+ z.synchronize_deleted_ca()
+ return redirect(resource_holder_list)
+ else:
+ form = forms.Empty()
+ return render(request, 'app/app_confirm_delete.html', {
+ 'form_title': 'Delete Resource Holder: ' + conf.handle,
+ 'form': form,
+ 'cancel_url': reverse(resource_holder_list)
+ })
+
+
+@superuser_required
+def resource_holder_create(request):
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ form = forms.ResourceHolderCreateForm(request.POST, request.FILES)
+ if form.is_valid():
+ handle = form.cleaned_data.get('handle')
+ parent = form.cleaned_data.get('parent')
+
+ zk_child = Zookeeper(handle=handle, logstream=log)
+ identity_xml = zk_child.initialize_resource_bpki()
+ if parent:
+ # FIXME etree_wrapper should allow us to deal with file objects
+ t = NamedTemporaryFile(delete=False)
+ t.close()
+
+ identity_xml.save(t.name)
+ zk_parent = Zookeeper(handle=parent.handle, logstream=log)
+ parent_response, _ = zk_parent.configure_child(t.name)
+ parent_response.save(t.name)
+ zk_parent.synchronize_ca()
+ repo_req, _ = zk_child.configure_parent(t.name)
+ repo_req.save(t.name)
+ repo_resp, _ = zk_parent.configure_publication_client(t.name)
+ repo_resp.save(t.name)
+ zk_parent.synchronize_pubd()
+ zk_child.configure_repository(t.name)
+ os.remove(t.name)
+ zk_child.synchronize_ca()
+ return redirect(resource_holder_list)
+ else:
+ form = forms.ResourceHolderCreateForm()
+ return render(request, 'app/app_form.html', {
+ 'form': form,
+ 'form_title': 'Create Resource Holder',
+ 'cancel_url': reverse(resource_holder_list)
+ })
+
+
+### views for managing user logins to the web interface
+
+@superuser_required
+def user_create(request):
+ if request.method == 'POST':
+ form = forms.UserCreateForm(request.POST, request.FILES)
+ if form.is_valid():
+ username = form.cleaned_data.get('username')
+ pw = form.cleaned_data.get('password')
+ email = form.cleaned_data.get('email')
+ user = User.objects.create_user(username, email, pw)
+ for conf in form.cleaned_data.get('resource_holders'):
+ models.ConfACL.objects.create(user=user, conf=conf)
+ return redirect(user_list)
+ else:
+ form = forms.UserCreateForm()
+
+ return render(request, 'app/app_form.html', {
+ 'form': form,
+ 'form_title': 'Create User',
+ 'cancel_url': reverse(user_list),
+ })
+
+
+@superuser_required
+def user_list(request):
+ """Display a list of all the RPKI handles managed by this server."""
+ return render(request, 'app/user_list.html', {
+ 'object_list': User.objects.all()
+ })
+
+
+@superuser_required
+def user_delete(request, pk):
+ user = get_object_or_404(User, pk=pk)
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ user.delete()
+ return redirect(user_list)
+ else:
+ form = forms.Empty()
+ return render(request, 'app/app_confirm_delete.html', {
+ 'form_title': 'Delete User: ' + user.username,
+ 'form': form,
+ 'cancel_url': reverse(user_list)
+ })
+
+
+@superuser_required
+def user_edit(request, pk):
+ user = get_object_or_404(User, pk=pk)
+ if request.method == 'POST':
+ form = forms.UserEditForm(request.POST)
+ if form.is_valid():
+ pw = form.cleaned_data.get('pw')
+ if pw:
+ user.set_password(pw)
+ user.email = form.cleaned_data.get('email')
+ user.save()
+ models.ConfACL.objects.filter(user=user).delete()
+ handles = form.cleaned_data.get('resource_holders')
+ for conf in handles:
+ models.ConfACL.objects.create(user=user, conf=conf)
+ return redirect(user_list)
+ else:
+ form = forms.UserEditForm(initial={
+ 'email': user.email,
+ 'resource_holders': models.Conf.objects.filter(confacl__user=user).all()
+ })
+ return render(request, 'app/app_form.html', {
+ 'form': form,
+ 'form_title': 'Edit User: ' + user.username,
+ 'cancel_url': reverse(user_list)
+ })
+
+
+class AlertListView(ListView):
+ # this nonsense is required to decorate CBVs
+ @method_decorator(handle_required)
+ def dispatch(self, request, *args, **kwargs):
+ return super(AlertListView, self).dispatch(request, *args, **kwargs)
+
+ def get_queryset(self, **kwargs):
+ conf = self.request.session['handle']
+ return conf.alerts.all()
+
+
+class AlertDetailView(DetailView):
+ # this nonsense is required to decorate CBVs
+ @method_decorator(handle_required)
+ def dispatch(self, request, *args, **kwargs):
+ return super(AlertDetailView, self).dispatch(request, *args, **kwargs)
+
+ 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')
+
+ # this nonsense is required to decorate CBVs
+ @method_decorator(handle_required)
+ def dispatch(self, request, *args, **kwargs):
+ return super(AlertDeleteView, self).dispatch(request, *args, **kwargs)
+
+ def get_queryset(self, **kwargs):
+ conf = self.request.session['handle']
+ return conf.alerts.all()
+
+
+@handle_required
+def alert_clear_all(request):
+ """Clear all alerts associated with the current resource holder."""
+ if request.method == 'POST':
+ form = forms.Empty(request.POST, request.FILES)
+ if form.is_valid():
+ # delete alerts
+ request.session['handle'].clear_alerts()
+ return redirect('alert-list')
+ else:
+ form = forms.Empty()
+ return render(request, 'app/alert_confirm_clear.html', {'form': form})
diff --git a/rpki/gui/cacheview/__init__.py b/rpki/gui/cacheview/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/cacheview/__init__.py
diff --git a/rpki/gui/cacheview/forms.py b/rpki/gui/cacheview/forms.py
new file mode 100644
index 00000000..28b8ff24
--- /dev/null
+++ b/rpki/gui/cacheview/forms.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django import forms
+
+from rpki.gui.cacheview.misc import parse_ipaddr
+from rpki.exceptions import BadIPResource
+from rpki.resource_set import resource_range_as
+
+
+class SearchForm(forms.Form):
+ asn = forms.CharField(required=False, help_text='AS or range', label='AS')
+ addr = forms.CharField(required=False, max_length=40, help_text='range/CIDR', label='IP Address')
+
+ def clean(self):
+ asn = self.cleaned_data.get('asn')
+ addr = self.cleaned_data.get('addr')
+ if (asn and addr) or ((not asn) and (not addr)):
+ raise forms.ValidationError, 'Please specify either an AS or IP range, not both'
+
+ if asn:
+ try:
+ resource_range_as.parse_str(asn)
+ except ValueError:
+ raise forms.ValidationError, 'invalid AS range'
+
+ if addr:
+ #try:
+ parse_ipaddr(addr)
+ #except BadIPResource:
+ # raise forms.ValidationError, 'invalid IP address range/prefix'
+
+ return self.cleaned_data
+
+
+class SearchForm2(forms.Form):
+ resource = forms.CharField(required=True)
diff --git a/rpki/gui/cacheview/misc.py b/rpki/gui/cacheview/misc.py
new file mode 100644
index 00000000..9a69645c
--- /dev/null
+++ b/rpki/gui/cacheview/misc.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+from rpki.resource_set import resource_range_ipv4, resource_range_ipv6
+from rpki.exceptions import BadIPResource
+
+def parse_ipaddr(s):
+ # resource_set functions only accept str
+ if isinstance(s, unicode):
+ s = s.encode()
+ s = s.strip()
+ r = resource_range_ipv4.parse_str(s)
+ try:
+ r = resource_range_ipv4.parse_str(s)
+ return 4, r
+ except BadIPResource:
+ r = resource_range_ipv6.parse_str(s)
+ return 6, r
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpki/gui/cacheview/models.py b/rpki/gui/cacheview/models.py
new file mode 100644
index 00000000..c3ee8421
--- /dev/null
+++ b/rpki/gui/cacheview/models.py
@@ -0,0 +1,237 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from datetime import datetime
+import time
+
+from django.db import models
+from django.core.urlresolvers import reverse
+
+import rpki.resource_set
+import rpki.gui.models
+
+
+class TelephoneField(models.CharField):
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = 255
+ models.CharField.__init__(self, *args, **kwargs)
+
+
+class AddressRange(rpki.gui.models.PrefixV4):
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.addressrange_detail', [str(self.pk)])
+
+
+class AddressRangeV6(rpki.gui.models.PrefixV6):
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.addressrange_detail_v6',
+ [str(self.pk)])
+
+
+class ASRange(rpki.gui.models.ASN):
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.asrange_detail', [str(self.pk)])
+
+kinds = list(enumerate(('good', 'warn', 'bad')))
+kinds_dict = dict((v, k) for k, v in kinds)
+
+
+class ValidationLabel(models.Model):
+ """
+ Represents a specific error condition defined in the rcynic XML
+ output file.
+ """
+ label = models.CharField(max_length=79, db_index=True, unique=True)
+ status = models.CharField(max_length=255)
+ kind = models.PositiveSmallIntegerField(choices=kinds)
+
+ def __unicode__(self):
+ return self.label
+
+
+class RepositoryObject(models.Model):
+ """
+ Represents a globally unique RPKI repository object, specified by its URI.
+ """
+ uri = models.URLField(unique=True, db_index=True)
+
+generations = list(enumerate(('current', 'backup')))
+generations_dict = dict((val, key) for (key, val) in generations)
+
+
+class ValidationStatus(models.Model):
+ timestamp = models.DateTimeField()
+ generation = models.PositiveSmallIntegerField(choices=generations, null=True)
+ status = models.ForeignKey(ValidationLabel)
+ repo = models.ForeignKey(RepositoryObject, related_name='statuses')
+
+
+class SignedObject(models.Model):
+ """
+ Abstract class to hold common metadata for all signed objects.
+ The signing certificate is ommitted here in order to give a proper
+ value for the 'related_name' attribute.
+ """
+ repo = models.ForeignKey(RepositoryObject, related_name='cert', unique=True)
+
+ # on-disk file modification time
+ mtime = models.PositiveIntegerField(default=0)
+
+ # SubjectName
+ name = models.CharField(max_length=255)
+
+ # value from the SKI extension
+ keyid = models.CharField(max_length=60, db_index=True)
+
+ # validity period from EE cert which signed object
+ not_before = models.DateTimeField()
+ not_after = models.DateTimeField()
+
+ def mtime_as_datetime(self):
+ """
+ convert the local timestamp to UTC and convert to a datetime object
+ """
+ return datetime.utcfromtimestamp(self.mtime + time.timezone)
+
+ def status_id(self):
+ """
+ Returns a HTML class selector for the current object based on its validation status.
+ The selector is chosen based on the current generation only. If there is any bad status,
+ return bad, else if there are any warn status, return warn, else return good.
+ """
+ for x in reversed(kinds):
+ if self.repo.statuses.filter(generation=generations_dict['current'], status__kind=x[0]):
+ return x[1]
+ return None # should not happen
+
+ def __unicode__(self):
+ return u'%s' % self.name
+
+
+class Cert(SignedObject):
+ """
+ Object representing a resource certificate.
+ """
+ addresses = models.ManyToManyField(AddressRange, related_name='certs')
+ addresses_v6 = models.ManyToManyField(AddressRangeV6, related_name='certs')
+ asns = models.ManyToManyField(ASRange, related_name='certs')
+ issuer = models.ForeignKey('self', related_name='children', null=True)
+ sia = models.CharField(max_length=255)
+
+ def get_absolute_url(self):
+ return reverse('cert-detail', args=[str(self.pk)])
+
+ def get_cert_chain(self):
+ """Return a list containing the complete certificate chain for this
+ certificate."""
+ cert = self
+ x = [cert]
+ while cert != cert.issuer:
+ cert = cert.issuer
+ x.append(cert)
+ x.reverse()
+ return x
+ cert_chain = property(get_cert_chain)
+
+
+class ROAPrefix(models.Model):
+ "Abstract base class for ROA mixin."
+
+ max_length = models.PositiveSmallIntegerField()
+
+ class Meta:
+ abstract = True
+
+ def as_roa_prefix(self):
+ "Return value as a rpki.resource_set.roa_prefix_ip object."
+ rng = self.as_resource_range()
+ return self.roa_cls(rng.min, rng.prefixlen(), self.max_length)
+
+ def __unicode__(self):
+ p = self.as_resource_range()
+ if p.prefixlen() == self.max_length:
+ return str(p)
+ return '%s-%s' % (str(p), self.max_length)
+
+
+# ROAPrefix is declared first, so subclass picks up __unicode__ from it.
+class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4):
+ "One v4 prefix in a ROA."
+
+ roa_cls = rpki.resource_set.roa_prefix_ipv4
+
+ @property
+ def routes(self):
+ """return all routes covered by this roa prefix"""
+ return RouteOrigin.objects.filter(prefix_min__gte=self.prefix_min,
+ prefix_max__lte=self.prefix_max)
+
+ class Meta:
+ ordering = ('prefix_min',)
+
+
+# ROAPrefix is declared first, so subclass picks up __unicode__ from it.
+class ROAPrefixV6(ROAPrefix, rpki.gui.models.PrefixV6):
+ "One v6 prefix in a ROA."
+
+ roa_cls = rpki.resource_set.roa_prefix_ipv6
+
+ class Meta:
+ ordering = ('prefix_min',)
+
+
+class ROA(SignedObject):
+ asid = models.PositiveIntegerField()
+ prefixes = models.ManyToManyField(ROAPrefixV4, related_name='roas')
+ prefixes_v6 = models.ManyToManyField(ROAPrefixV6, related_name='roas')
+ issuer = models.ForeignKey('Cert', related_name='roas')
+
+ def get_absolute_url(self):
+ return reverse('roa-detail', args=[str(self.pk)])
+
+ class Meta:
+ ordering = ('asid',)
+
+ def __unicode__(self):
+ return u'ROA for AS%d' % self.asid
+
+
+class Ghostbuster(SignedObject):
+ full_name = models.CharField(max_length=40)
+ email_address = models.EmailField(blank=True, null=True)
+ organization = models.CharField(blank=True, null=True, max_length=255)
+ telephone = TelephoneField(blank=True, null=True)
+ issuer = models.ForeignKey('Cert', related_name='ghostbusters')
+
+ def get_absolute_url(self):
+ # note that ghostbuster-detail is different from gbr-detail! sigh
+ return reverse('ghostbuster-detail', args=[str(self.pk)])
+
+ def __unicode__(self):
+ if self.full_name:
+ return self.full_name
+ if self.organization:
+ return self.organization
+ if self.email_address:
+ return self.email_address
+ return self.telephone
+
+
+from rpki.gui.routeview.models import RouteOrigin
diff --git a/rpki/gui/cacheview/templates/cacheview/addressrange_detail.html b/rpki/gui/cacheview/templates/cacheview/addressrange_detail.html
new file mode 100644
index 00000000..76edc1ba
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/addressrange_detail.html
@@ -0,0 +1,18 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+<h1>{% block title %}IP Range Detail{% endblock %}</h1>
+
+<p>
+IP Range: {{ object }}
+</p>
+
+<p>Covered by the following resource certs:</p>
+
+<ul>
+{% for cert in object.certs.all %}
+<li><a href="{{ cert.get_absolute_url }}">{{ cert }}</a></li>
+{% endfor %}
+</ul>
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/cacheview_base.html b/rpki/gui/cacheview/templates/cacheview/cacheview_base.html
new file mode 100644
index 00000000..ec71d740
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/cacheview_base.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+{% load url from future %}
+
+{% block sidebar %}
+<form method='post' action='{% url 'res-search' %}'>
+ {% csrf_token %}
+ <input type='text' id='id_resource' name='resource' placeholder='prefix or AS'>
+ <button type='submit'>Search</button>
+</form>
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/cert_detail.html b/rpki/gui/cacheview/templates/cacheview/cert_detail.html
new file mode 100644
index 00000000..256e7780
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/cert_detail.html
@@ -0,0 +1,105 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}
+Resource Certificate Detail
+{% endblock %}
+
+{% block detail %}
+
+<h2>RFC3779 Resources</h2>
+
+<table class='table table-striped'>
+ <thead>
+ <tr><th>AS Ranges</th><th>IP Ranges</th></tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td style='text-align:left;vertical-align:top'>
+ <ul class='compact'>
+ {% for asn in object.asns.all %}
+ <li><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></li>
+ {% endfor %}
+ </ul>
+ </td>
+ <td style='text-align:left;vertical-align:top'>
+ <ul class='compact'>
+ {% for rng in object.addresses.all %}
+ <li><a href="{{ rng.get_absolute_url }}">{{ rng }}</a></li>
+ {% endfor %}
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<div class='section'>
+<h2>Issued Objects</h2>
+<ul>
+
+{% if object.ghostbusters.all %}
+ <li>
+<h3>Ghostbusters</h3>
+
+<table class='table table-striped'>
+ <thead>
+ <tr><th>Name</th><th>Expires</th></tr>
+ </thead>
+ <tbody>
+
+{% for g in object.ghostbusters.all %}
+ <tr class='{{ g.status_id }}'>
+ <td><a href="{{ g.get_absolute_url }}">{{ g }}</a></td>
+ <td>{{ g.not_after }}</td>
+ </tr>
+ </tbody>
+{% endfor %}
+
+</table>
+{% endif %}
+
+{% if object.roas.all %}
+ <li>
+<h3>ROAs</h3>
+<table class='table table-striped'>
+ <thead>
+ <tr><th>#</th><th>Prefix</th><th>AS</th><th>Expires</th></tr>
+ </thead>
+ <tbody>
+ {% for roa in object.roas.all %}
+ {% for pfx in roa.prefixes.all %}
+ <tr class='{{ roa.status_id }}'>
+ <td><a href="{{ roa.get_absolute_url }}">#</a></td>
+ <td>{{ pfx }}</td>
+ <td>{{ roa.asid }}</td>
+ <td>{{ roa.not_after }}</td>
+ </tr>
+ {% endfor %}
+ {% endfor %}
+ </tbody>
+</table>
+{% endif %}
+
+{% if object.children.all %}
+<li>
+<h3>Children</h3>
+<table class='table table-striped'>
+ <thead>
+ <tr><th>Name</th><th>Expires</th></tr>
+ </thead>
+ <tbody>
+
+ {% for child in object.children.all %}
+ <tr class='{{ child.status_id }}'>
+ <td><a href="{{ child.get_absolute_url }}">{{ child.name }}</a></td>
+ <td>{{ child.not_after }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endif %}
+
+</ul>
+
+</div><!--issued objects-->
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html b/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html
new file mode 100644
index 00000000..4215f757
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html
@@ -0,0 +1,13 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}Ghostbuster Detail{% endblock %}
+
+{% block detail %}
+<p>
+<table class='table'>
+ <tr><td>Full Name</td><td>{{ object.full_name }}</td></tr>
+ <tr><td>Organization</td><td>{{ object.organization }}</td></tr>
+ <tr><td>Email</td><td>{{ object.email_address }}</td></tr>
+ <tr><td>Telephone</td><td>{{ object.telephone }}</td></tr>
+</table>
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/global_summary.html b/rpki/gui/cacheview/templates/cacheview/global_summary.html
new file mode 100644
index 00000000..0dbd0ffc
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/global_summary.html
@@ -0,0 +1,26 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Browse Global RPKI</h1>
+</div>
+
+<table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Expires</th>
+ <th>URI</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for r in roots %}
+ <tr>
+ <td><a href="{{ r.get_absolute_url }}">{{ r.name }}</a></td>
+ <td>{{ r.not_after }}</td>
+ <td>{{ r.repo.uri }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock content %}
diff --git a/rpki/gui/cacheview/templates/cacheview/query_result.html b/rpki/gui/cacheview/templates/cacheview/query_result.html
new file mode 100644
index 00000000..0694c531
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/query_result.html
@@ -0,0 +1,21 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+
+<h1>{% block title %}Query Results{% endblock %}</h1>
+
+<table>
+ <tr><th>Prefix</th><th>AS</th><th>Valid</th><th>Until</th></tr>
+ {% for object in object_list %}
+ <tr class='{{ object.1.status.kind_as_str }}'>
+ <td>{{ object.0 }}</td>
+ <td>{{ object.1.asid }}</td>
+ <td><a href="{{ object.1.get_absolute_url }}">{{ object.1.ok }}</a></td>
+ <td>{{ object.1.not_after }}</td>
+ </tr>
+ {% endfor %}
+</table>
+
+<p><a href="{% url rpki.gui.cacheview.views.query_view %}">new query</a></p>
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/roa_detail.html b/rpki/gui/cacheview/templates/cacheview/roa_detail.html
new file mode 100644
index 00000000..39cc547b
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/roa_detail.html
@@ -0,0 +1,18 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}ROA Detail{% endblock %}
+
+{% block detail %}
+<p>
+<table>
+ <tr><td>AS</td><td>{{ object.asid }}</td></tr>
+</table>
+
+<h2>Prefixes</h2>
+
+<ul>
+{% for pfx in object.prefixes.all %}
+<li>{{ pfx }}
+{% endfor %}
+</ul>
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/search_form.html b/rpki/gui/cacheview/templates/cacheview/search_form.html
new file mode 100644
index 00000000..1141615d
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/search_form.html
@@ -0,0 +1,17 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block title %}
+{{ search_type }} Search
+{% endblock %}
+
+{% block content %}
+
+<h1>{{search_type}} Search</h1>
+
+<form method='post' action='{{ request.url }}'>
+ {% csrf_token %}
+ {{ form.as_p }}
+ <input type='submit' name='Search'>
+</form>
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/search_result.html b/rpki/gui/cacheview/templates/cacheview/search_result.html
new file mode 100644
index 00000000..7cbf852e
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/search_result.html
@@ -0,0 +1,42 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Search Results <small>{{ resource }}</small></h1>
+</div>
+
+<h2>Matching Resource Certificates</h2>
+{% if certs %}
+<ul>
+{% for cert in certs %}
+<li><a href="{{ cert.get_absolute_url }}">{{ cert }}</a>
+{% endfor %}
+</ul>
+{% else %}
+<p>none</p>
+{% endif %}
+
+<h2>Matching ROAs</h2>
+{% if roas %}
+<table class='table table-striped'>
+ <thead>
+ <tr>
+ <th>#</th><th>Prefix</th><th>AS</th>
+ </tr>
+ </thead>
+ <tbody>
+{% for roa in roas %}
+<tr>
+ <td><a href="{{ roa.get_absolute_url }}">#</a></td>
+ <td>{{ roa.prefixes.all.0 }}</td>
+ <td>{{ roa.asid }}</td>
+</tr>
+{% endfor %}
+</tbody>
+</table>
+{% else %}
+<p>none</p>
+{% endif %}
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html b/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html
new file mode 100644
index 00000000..22ae3d27
--- /dev/null
+++ b/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html
@@ -0,0 +1,58 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+<div class='page-header'>
+<h1>{% block title %}Signed Object Detail{% endblock %}</h1>
+</div>
+
+<h2>Cert Info</h2>
+<table class='table table-striped'>
+ <tr><td>Subject Name</td><td>{{ object.name }}</td></tr>
+ <tr><td>SKI</td><td>{{ object.keyid }}</td></tr>
+ {% if object.sia %}
+ <tr><td>SIA</td><td>{{ object.sia }}</td></tr>
+ {% endif %}
+ <tr><td>Not Before</td><td>{{ object.not_before }}</td></tr>
+ <tr><td>Not After</td><td>{{ object.not_after }}</td></tr>
+</table>
+
+<h2>Metadata</h2>
+
+<table class='table table-striped'>
+ <tr><td>URI</td><td>{{ object.repo.uri }}</td></tr>
+ <tr><td>Last Modified</td><td>{{ object.mtime_as_datetime|date:"DATETIME_FORMAT" }}</td></tr>
+</table>
+
+<h2>Validation Status</h2>
+<table class='table table-striped'>
+ <thead>
+ <tr><th>Timestamp</th><th>Generation</th><th>Status</th></tr>
+ </thead>
+ <tbody>
+ {% for status in object.repo.statuses.all %}
+ <tr class="{{ status.status.get_kind_display }}"><td>{{ status.timestamp }}</td><td>{{ status.get_generation_display }}</td><td>{{ status.status.status }}</td></tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+<h2>X.509 Certificate Chain</h2>
+
+<table class='table table-striped'>
+ <thead>
+ <tr><th>Depth</th><th>Name</th></tr>
+ </thead>
+ <tbody>
+
+{% for cert in chain %}
+<tr class='{{ cert.1.status_id }}'>
+ <td>{{ cert.0 }}</td>
+ <td><a href="{{ cert.1.get_absolute_url }}">{{ cert.1.name }}</a></td>
+</tr>
+{% endfor %}
+</tbody>
+
+</table>
+
+{% block detail %}{% endblock %}
+
+{% endblock %}
diff --git a/rpki/gui/cacheview/tests.py b/rpki/gui/cacheview/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/rpki/gui/cacheview/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/rpki/gui/cacheview/urls.py b/rpki/gui/cacheview/urls.py
new file mode 100644
index 00000000..cc03a587
--- /dev/null
+++ b/rpki/gui/cacheview/urls.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.conf.urls import patterns, url
+from rpki.gui.cacheview.views import (CertDetailView, RoaDetailView,
+ GhostbusterDetailView)
+
+urlpatterns = patterns('',
+ url(r'^search$', 'rpki.gui.cacheview.views.search_view',
+ name='res-search'),
+ url(r'^cert/(?P<pk>[^/]+)$', CertDetailView.as_view(), name='cert-detail'),
+ url(r'^gbr/(?P<pk>[^/]+)$', GhostbusterDetailView.as_view(),
+ name='ghostbuster-detail'),
+ url(r'^roa/(?P<pk>[^/]+)$', RoaDetailView.as_view(), name='roa-detail'),
+ (r'^$', 'rpki.gui.cacheview.views.global_summary'),
+)
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpki/gui/cacheview/util.py b/rpki/gui/cacheview/util.py
new file mode 100644
index 00000000..0d3d7ae3
--- /dev/null
+++ b/rpki/gui/cacheview/util.py
@@ -0,0 +1,432 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham
+# Copyright (C) 2012, 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+__all__ = ('import_rcynic_xml')
+
+default_logfile = '/var/rcynic/data/rcynic.xml'
+default_root = '/var/rcynic/data'
+object_accepted = None # set by import_rcynic_xml()
+
+import time
+import vobject
+import logging
+import os
+import stat
+from socket import getfqdn
+from cStringIO import StringIO
+
+from django.db import transaction
+import django.db.models
+
+import rpki
+import rpki.gui.app.timestamp
+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
+from rpki.irdb.zookeeper import Zookeeper
+
+logger = logging.getLogger(__name__)
+
+
+def rcynic_cert(cert, obj):
+ obj.sia = cert.sia_directory_uri
+
+ # object must be saved for the related manager methods below to work
+ obj.save()
+
+ # for the root cert, we can't set inst.issuer = inst until
+ # after inst.save() has been called.
+ if obj.issuer is None:
+ obj.issuer = obj
+ obj.save()
+
+ # resources can change when a cert is updated
+ obj.asns.clear()
+ obj.addresses.clear()
+
+ if cert.resources.asn.inherit:
+ # FIXME: what happens when the parent's resources change and the child
+ # cert is not reissued?
+ obj.asns.add(*obj.issuer.asns.all())
+ else:
+ for asr in cert.resources.asn:
+ logger.debug('processing %s' % asr)
+
+ attrs = {'min': asr.min, 'max': asr.max}
+ q = models.ASRange.objects.filter(**attrs)
+ if not q:
+ obj.asns.create(**attrs)
+ else:
+ obj.asns.add(q[0])
+
+ # obj.issuer is None the first time we process the root cert in the
+ # hierarchy, so we need to guard against dereference
+ for cls, addr_obj, addrset, parentset in (
+ models.AddressRange, obj.addresses, cert.resources.v4,
+ obj.issuer.addresses.all() if obj.issuer else []
+ ), (
+ models.AddressRangeV6, obj.addresses_v6, cert.resources.v6,
+ obj.issuer.addresses_v6.all() if obj.issuer else []
+ ):
+ if addrset.inherit:
+ addr_obj.add(*parentset)
+ else:
+ for rng in addrset:
+ logger.debug('processing %s' % rng)
+
+ attrs = {'prefix_min': rng.min, 'prefix_max': rng.max}
+ q = cls.objects.filter(**attrs)
+ if not q:
+ addr_obj.create(**attrs)
+ else:
+ addr_obj.add(q[0])
+
+
+def rcynic_roa(roa, obj):
+ obj.asid = roa.asID
+ # object must be saved for the related manager methods below to work
+ obj.save()
+ obj.prefixes.clear()
+ obj.prefixes_v6.clear()
+ for pfxset in roa.prefix_sets:
+ if pfxset.__class__.__name__ == 'roa_prefix_set_ipv6':
+ roa_cls = models.ROAPrefixV6
+ prefix_obj = obj.prefixes_v6
+ else:
+ roa_cls = models.ROAPrefixV4
+ prefix_obj = obj.prefixes
+
+ for pfx in pfxset:
+ attrs = {'prefix_min': pfx.min(),
+ 'prefix_max': pfx.max(),
+ 'max_length': pfx.max_prefixlen}
+ q = roa_cls.objects.filter(**attrs)
+ if not q:
+ prefix_obj.create(**attrs)
+ else:
+ prefix_obj.add(q[0])
+
+
+def rcynic_gbr(gbr, obj):
+ vcard = vobject.readOne(gbr.vcard)
+ obj.full_name = vcard.fn.value if hasattr(vcard, 'fn') else None
+ obj.email_address = vcard.email.value if hasattr(vcard, 'email') else None
+ obj.telephone = vcard.tel.value if hasattr(vcard, 'tel') else None
+ obj.organization = vcard.org.value[0] if hasattr(vcard, 'org') else None
+ obj.save()
+
+LABEL_CACHE = {}
+
+# dict keeping mapping of uri to (handle, old status, new status) for objects
+# published by the local rpkid
+uris = {}
+
+dispatch = {
+ 'rcynic_certificate': rcynic_cert,
+ 'rcynic_roa': rcynic_roa,
+ 'rcynic_ghostbuster': rcynic_gbr
+}
+
+model_class = {
+ 'rcynic_certificate': models.Cert,
+ 'rcynic_roa': models.ROA,
+ 'rcynic_ghostbuster': models.Ghostbuster
+}
+
+
+def save_status(repo, vs):
+ timestamp = datetime.fromXMLtime(vs.timestamp).to_sql()
+ status = LABEL_CACHE[vs.status]
+ g = models.generations_dict[vs.generation] if vs.generation else None
+ repo.statuses.create(generation=g, timestamp=timestamp, status=status)
+
+ # if this object is in our interest set, update with the current validation
+ # status
+ if repo.uri in uris:
+ x, y, z, q = uris[repo.uri]
+ valid = z or (status is object_accepted) # don't clobber previous True value
+ uris[repo.uri] = x, y, valid, repo
+
+ if status is not object_accepted:
+ return
+
+ cls = model_class[vs.file_class.__name__]
+ # find the instance of the signedobject subclass that is associated with
+ # this repo instance (may be empty when not accepted)
+ inst_qs = cls.objects.filter(repo=repo)
+
+ logger.debug('processing %s' % vs.filename)
+
+ if not inst_qs:
+ inst = cls(repo=repo)
+ logger.debug('object not found in db, creating new object cls=%s id=%s' % (
+ cls,
+ id(inst)
+ ))
+ else:
+ inst = inst_qs[0]
+
+ try:
+ # determine if the object is changed/new
+ mtime = os.stat(vs.filename)[stat.ST_MTIME]
+ except OSError as e:
+ logger.error('unable to stat %s: %s %s' % (
+ vs.filename, type(e), e))
+ # treat as if missing from rcynic.xml
+ # use inst_qs rather than deleting inst so that we don't raise an
+ # exception for newly created objects (inst_qs will be empty)
+ inst_qs.delete()
+ return
+
+ if mtime != inst.mtime:
+ inst.mtime = mtime
+ try:
+ obj = vs.obj # causes object to be lazily loaded
+ except Exception, e:
+ logger.warning('Caught %s while processing %s: %s' % (
+ type(e), vs.filename, e))
+ return
+
+ inst.not_before = obj.notBefore.to_sql()
+ inst.not_after = obj.notAfter.to_sql()
+ inst.name = obj.subject
+ inst.keyid = obj.ski
+
+ # look up signing cert
+ if obj.issuer == obj.subject:
+ # self-signed cert (TA)
+ assert(isinstance(inst, models.Cert))
+ inst.issuer = None
+ else:
+ # if an object has moved in the repository, the entry for
+ # the old location will still be in the database, but
+ # without any object_accepted in its validtion status
+ qs = models.Cert.objects.filter(
+ keyid=obj.aki,
+ name=obj.issuer,
+ repo__statuses__status=object_accepted
+ )
+ ncerts = len(qs)
+ if ncerts == 0:
+ logger.warning('unable to find signing cert with ski=%s (%s)' % (obj.aki, obj.issuer))
+ return
+ else:
+ if ncerts > 1:
+ # multiple matching certs, all of which are valid
+ logger.warning('Found multiple certs matching ski=%s sn=%s' % (obj.aki, obj.issuer))
+ for c in qs:
+ logger.warning(c.repo.uri)
+ # just use the first match
+ inst.issuer = qs[0]
+
+ try:
+ # do object-specific tasks
+ dispatch[vs.file_class.__name__](obj, inst)
+ except:
+ logger.error('caught exception while processing rcynic_object:\n'
+ 'vs=' + repr(vs) + '\nobj=' + repr(obj))
+ # .show() writes to stdout
+ obj.show()
+ raise
+
+ logger.debug('object saved id=%s' % id(inst))
+ else:
+ logger.debug('object is unchanged')
+
+
+@transaction.commit_on_success
+def process_cache(root, xml_file):
+
+ last_uri = None
+ repo = None
+
+ logger.info('clearing validation statuses')
+ models.ValidationStatus.objects.all().delete()
+
+ logger.info('updating validation status')
+ for vs in rcynic_xml_iterator(root, xml_file):
+ if vs.uri != last_uri:
+ repo, created = models.RepositoryObject.objects.get_or_create(uri=vs.uri)
+ last_uri = vs.uri
+ save_status(repo, vs)
+
+ # garbage collection
+ # remove all objects which have no ValidationStatus references, which
+ # means they did not appear in the last XML output
+ logger.info('performing garbage collection')
+
+ # Delete all objects that have zero validation status elements.
+ models.RepositoryObject.objects.annotate(num_statuses=django.db.models.Count('statuses')).filter(num_statuses=0).delete()
+
+ # Delete all SignedObject instances that were not accepted. There may
+ # exist rows for objects that were previously accepted.
+ # See https://trac.rpki.net/ticket/588#comment:30
+ #
+ # We have to do this here rather than in save_status() because the
+ # <validation_status/> elements are not guaranteed to be consecutive for a
+ # given URI. see https://trac.rpki.net/ticket/625#comment:5
+ models.SignedObject.objects.exclude(repo__statuses__status=object_accepted).delete()
+
+ # ROAPrefixV* objects are M2M so they are not automatically deleted when
+ # their ROA object disappears
+ models.ROAPrefixV4.objects.annotate(num_roas=django.db.models.Count('roas')).filter(num_roas=0).delete()
+ models.ROAPrefixV6.objects.annotate(num_roas=django.db.models.Count('roas')).filter(num_roas=0).delete()
+ logger.info('done with garbage collection')
+
+
+@transaction.commit_on_success
+def process_labels(xml_file):
+ logger.info('updating labels...')
+
+ for label, kind, desc in label_iterator(xml_file):
+ logger.debug('label=%s kind=%s desc=%s' % (label, kind, desc))
+ if kind:
+ q = models.ValidationLabel.objects.filter(label=label)
+ if not q:
+ obj = models.ValidationLabel(label=label)
+ else:
+ obj = q[0]
+
+ obj.kind = models.kinds_dict[kind]
+ obj.status = desc
+ obj.save()
+
+ LABEL_CACHE[label] = obj
+
+
+def fetch_published_objects():
+ """Query rpkid for all objects published by local users, and look up the
+ current validation status of each object. The validation status is used
+ later to send alerts for objects which have transitioned to invalid.
+
+ """
+ logger.info('querying for published objects')
+
+ handles = [conf.handle for conf in Conf.objects.all()]
+ req = [rpki.left_right.list_published_objects_elt.make_pdu(action='list', self_handle=h, tag=h) for h in handles]
+ z = Zookeeper()
+ pdus = z.call_rpkid(*req)
+ for pdu in pdus:
+ if isinstance(pdu, rpki.left_right.list_published_objects_elt):
+ # Look up the object in the rcynic cache
+ qs = models.RepositoryObject.objects.filter(uri=pdu.uri)
+ if qs:
+ # get the current validity state
+ valid = qs[0].statuses.filter(status=object_accepted).exists()
+ uris[pdu.uri] = (pdu.self_handle, valid, False, None)
+ logger.debug('adding ' + pdu.uri)
+ else:
+ # this object is not in the cache. it was either published
+ # recently, or disappared previously. if it disappeared
+ # previously, it has already been alerted. in either case, we
+ # omit the uri from the list since we are interested only in
+ # objects which were valid and are no longer valid
+ pass
+ elif isinstance(pdu, rpki.left_right.report_error_elt):
+ logging.error('rpkid reported an error: %s' % pdu.error_code)
+
+
+class Handle(object):
+ def __init__(self):
+ self.invalid = []
+ self.missing = []
+
+ def add_invalid(self, v):
+ self.invalid.append(v)
+
+ def add_missing(self, v):
+ self.missing.append(v)
+
+
+def notify_invalid():
+ """Send email alerts to the addresses registered in ghostbuster records for
+ any invalid objects that were published by users of this system.
+
+ """
+
+ logger.info('sending notifications for invalid objects')
+
+ # group invalid objects by user
+ notify = {}
+ for uri, v in uris.iteritems():
+ handle, old_status, new_status, obj = v
+
+ if obj is None:
+ # object went missing
+ n = notify.get(handle, Handle())
+ n.add_missing(uri)
+ # only select valid->invalid
+ elif old_status and not new_status:
+ n = notify.get(handle, Handle())
+ n.add_invalid(obj)
+
+ for handle, v in notify.iteritems():
+ conf = Conf.objects.get(handle)
+
+ msg = StringIO()
+ msg.write('This is an alert about problems with objects published by '
+ 'the resource handle %s.\n\n' % handle)
+
+ if v.invalid:
+ msg.write('The following objects were previously valid, but are '
+ 'now invalid:\n')
+
+ for o in v.invalid:
+ msg.write('\n')
+ msg.write(o.repo.uri)
+ msg.write('\n')
+ for s in o.statuses.all():
+ msg.write('\t')
+ msg.write(s.status.label)
+ msg.write(': ')
+ msg.write(s.status.status)
+ msg.write('\n')
+
+ if v.missing:
+ msg.write('The following objects were previously valid but are no '
+ 'longer in the cache:\n')
+
+ for o in v.missing:
+ msg.write(o)
+ msg.write('\n')
+
+ msg.write("""--
+You are receiving this email because your address is published in a Ghostbuster
+record, or is the default email address for this resource holder account on
+%s.""" % getfqdn())
+
+ from_email = 'root@' + getfqdn()
+ subj = 'invalid RPKI object alert for resource handle %s' % conf.handle
+ conf.send_alert(subj, msg.getvalue(), from_email, severity=Alert.ERROR)
+
+
+def import_rcynic_xml(root=default_root, logfile=default_logfile):
+ """Load the contents of rcynic.xml into the rpki.gui.cacheview database."""
+
+ global object_accepted
+
+ start = time.time()
+ process_labels(logfile)
+ object_accepted = LABEL_CACHE['object_accepted']
+ fetch_published_objects()
+ process_cache(root, logfile)
+ notify_invalid()
+
+ rpki.gui.app.timestamp.update('rcynic_import')
+
+ stop = time.time()
+ logger.info('elapsed time %d seconds.' % (stop - start))
diff --git a/rpki/gui/cacheview/views.py b/rpki/gui/cacheview/views.py
new file mode 100644
index 00000000..94870eb2
--- /dev/null
+++ b/rpki/gui/cacheview/views.py
@@ -0,0 +1,172 @@
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.views.generic import DetailView
+from django.shortcuts import render
+from django.db.models import F
+
+from rpki.gui.cacheview import models, forms, misc
+from rpki.resource_set import resource_range_as, resource_range_ip
+from rpki.POW import IPAddress
+from rpki.exceptions import BadIPResource
+
+
+def cert_chain(obj):
+ """
+ returns an iterator covering all certs from the root cert down to the EE.
+ """
+ chain = [obj]
+ while obj != obj.issuer:
+ obj = obj.issuer
+ chain.append(obj)
+ return zip(range(len(chain)), reversed(chain))
+
+
+class SignedObjectDetailView(DetailView):
+ def get_context_data(self, **kwargs):
+ context = super(SignedObjectDetailView,
+ self).get_context_data(**kwargs)
+ context['chain'] = cert_chain(self.object)
+ return context
+
+
+class RoaDetailView(SignedObjectDetailView):
+ model = models.ROA
+
+
+class CertDetailView(SignedObjectDetailView):
+ model = models.Cert
+
+
+class GhostbusterDetailView(SignedObjectDetailView):
+ model = models.Ghostbuster
+
+
+def search_view(request):
+ certs = None
+ roas = None
+
+ if request.method == 'POST':
+ form = forms.SearchForm2(request.POST, request.FILES)
+ if form.is_valid():
+ resource = form.cleaned_data.get('resource')
+ # try to determine the type of input given
+ try:
+ r = resource_range_as.parse_str(resource)
+ certs = models.Cert.objects.filter(asns__min__gte=r.min,
+ asns__max__lte=r.max)
+ roas = models.ROA.objects.filter(asid__gte=r.min,
+ asid__lte=r.max)
+ except:
+ try:
+ r = resource_range_ip.parse_str(resource)
+ if r.version == 4:
+ certs = models.Cert.objects.filter(
+ addresses__prefix_min__lte=r.min,
+ addresses__prefix_max__gte=r.max)
+ roas = models.ROA.objects.filter(
+ prefixes__prefix_min__lte=r.min,
+ prefixes__prefix_max__gte=r.max)
+ else:
+ certs = models.Cert.objects.filter(
+ addresses_v6__prefix_min__lte=r.min,
+ addresses_v6__prefix_max__gte=r.max)
+ roas = models.ROA.objects.filter(
+ prefixes_v6__prefix_min__lte=r.min,
+ prefixes_v6__prefix_max__gte=r.max)
+ except BadIPResource:
+ pass
+
+ return render(request, 'cacheview/search_result.html',
+ {'resource': resource, 'certs': certs, 'roas': roas})
+
+
+def cmp_prefix(x, y):
+ r = cmp(x[0].family, y[0].family)
+ if r == 0:
+ r = cmp(x[2], y[2]) # integer address
+ if r == 0:
+ r = cmp(x[0].bits, y[0].bits)
+ if r == 0:
+ r = cmp(x[0].max_length, y[0].max_length)
+ if r == 0:
+ r = cmp(x[1].asid, y[1].asid)
+ return r
+
+
+#def cmp_prefix(x,y):
+# for attr in ('family', 'prefix', 'bits', 'max_length'):
+# r = cmp(getattr(x[0], attr), getattr(y[0], attr))
+# if r:
+# return r
+# return cmp(x[1].asid, y[1].asid)
+
+
+def query_view(request):
+ """
+ Allow the user to search for an AS or prefix, and show all published ROA
+ information.
+ """
+
+ if request.method == 'POST':
+ form = forms.SearchForm(request.POST, request.FILES)
+ if form.is_valid():
+ certs = None
+ roas = None
+
+ addr = form.cleaned_data.get('addr')
+ asn = form.cleaned_data.get('asn')
+
+ if addr:
+ family, r = misc.parse_ipaddr(addr)
+ prefixes = models.ROAPrefix.objects.filter(family=family, prefix=str(r.min))
+
+ prefix_list = []
+ for pfx in prefixes:
+ for roa in pfx.roas.all():
+ prefix_list.append((pfx, roa))
+ elif asn:
+ r = resource_range_as.parse_str(asn)
+ roas = models.ROA.objects.filter(asid__gte=r.min, asid__lte=r.max)
+
+ # display the results sorted by prefix
+ prefix_list = []
+ for roa in roas:
+ for pfx in roa.prefixes.all():
+ addr = IPAddress(pfx.prefix.encode())
+ prefix_list.append((pfx, roa, addr))
+ prefix_list.sort(cmp=cmp_prefix)
+
+ return render('cacheview/query_result.html',
+ {'object_list': prefix_list}, request)
+ else:
+ form = forms.SearchForm()
+
+ return render('cacheview/search_form.html', {
+ 'form': form, 'search_type': 'ROA '}, request)
+
+
+def global_summary(request):
+ """Display a table summarizing the state of the global RPKI."""
+
+ roots = models.Cert.objects.filter(issuer=F('pk')) # self-signed
+
+ return render(request, 'cacheview/global_summary.html', {
+ 'roots': roots
+ })
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpki/gui/decorators.py b/rpki/gui/decorators.py
new file mode 100644
index 00000000..69d20c46
--- /dev/null
+++ b/rpki/gui/decorators.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django import http
+
+
+def tls_required(f):
+ """Decorator which returns a 500 error if the connection is not secured
+ with TLS (https).
+
+ """
+ def _tls_required(request, *args, **kwargs):
+ if not request.is_secure():
+ return http.HttpResponseServerError(
+ 'This resource may only be accessed securely via https',
+ content_type='text/plain')
+ return f(request, *args, **kwargs)
+ return _tls_required
diff --git a/rpki/gui/default_settings.py b/rpki/gui/default_settings.py
new file mode 100644
index 00000000..3859247c
--- /dev/null
+++ b/rpki/gui/default_settings.py
@@ -0,0 +1,171 @@
+"""
+This module contains static configuration settings for the web portal.
+"""
+
+__version__ = '$Id$'
+
+import os
+import random
+import string
+import socket
+
+import rpki.config
+import rpki.autoconf
+
+# Where to put static files.
+STATIC_ROOT = rpki.autoconf.datarootdir + '/rpki/media'
+
+# Must end with a slash!
+STATIC_URL = '/media/'
+
+# Where to email server errors.
+ADMINS = (('Administrator', 'root@localhost'),)
+
+LOGGING = {
+ 'version': 1,
+ 'formatters': {
+ 'verbose': {
+ # see http://docs.python.org/2.7/library/logging.html#logging.LogRecord
+ 'format': '%(levelname)s %(asctime)s %(name)s %(message)s'
+ },
+ },
+ 'handlers': {
+ 'stderr': {
+ 'class': 'logging.StreamHandler',
+ 'level': 'DEBUG',
+ 'formatter': 'verbose',
+ },
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'class': 'django.utils.log.AdminEmailHandler',
+ },
+ },
+ 'loggers': {
+ 'django': {
+ 'level': 'ERROR',
+ 'handlers': ['stderr', 'mail_admins'],
+ },
+ 'rpki.gui': {
+ 'level': 'WARNING',
+ 'handlers': ['stderr'],
+ },
+ },
+}
+
+# Load the SQL authentication bits from the system rpki.conf.
+rpki_config = rpki.config.parser(section='web_portal')
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': rpki_config.get('sql-database'),
+ 'USER': rpki_config.get('sql-username'),
+ 'PASSWORD': rpki_config.get('sql-password'),
+
+ # Ensure the default storage engine is InnoDB since we need
+ # foreign key support. The Django documentation suggests
+ # removing this after the syncdb is performed as an optimization,
+ # but there isn't an easy way to do this automatically.
+
+ 'OPTIONS': {
+ 'init_command': 'SET storage_engine=INNODB',
+ }
+ }
+}
+
+
+def select_tz():
+ "Find a supported timezone that looks like UTC"
+ for tz in ('UTC', 'GMT', 'Etc/UTC', 'Etc/GMT'):
+ if os.path.exists('/usr/share/zoneinfo/' + tz):
+ return tz
+ # Can't determine the proper timezone, fall back to UTC and let Django
+ # report the error to the user.
+ return 'UTC'
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = select_tz()
+
+def get_secret_key():
+ """Retrieve the secret-key value from rpki.conf or generate a random value
+ if it is not present."""
+ d = string.letters + string.digits
+ val = ''.join([random.choice(d) for _ in range(50)])
+ return rpki_config.get('secret-key', val)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = get_secret_key()
+
+# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
+# for details on why you might need this.
+def get_allowed_hosts():
+ allowed_hosts = set(rpki_config.multiget("allowed-hosts"))
+ allowed_hosts.add(socket.getfqdn())
+ try:
+ import netifaces
+ for interface in netifaces.interfaces():
+ addresses = netifaces.ifaddresses(interface)
+ for af in (netifaces.AF_INET, netifaces.AF_INET6):
+ if af in addresses:
+ for address in addresses[af]:
+ if "addr" in address:
+ allowed_hosts.add(address["addr"])
+ except ImportError:
+ pass
+ return list(allowed_hosts)
+
+ALLOWED_HOSTS = get_allowed_hosts()
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ 'django.template.loaders.eggs.Loader'
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware'
+)
+
+ROOT_URLCONF = 'rpki.gui.urls'
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ #'django.contrib.admin',
+ #'django.contrib.admindocs',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.staticfiles',
+ 'rpki.irdb',
+ 'rpki.gui.app',
+ 'rpki.gui.cacheview',
+ 'rpki.gui.routeview',
+ 'south',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.contrib.auth.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.contrib.messages.context_processors.messages",
+ "django.core.context_processors.request",
+ "django.core.context_processors.static"
+)
+
+# Allow local site to override any setting above -- but if there's
+# anything that local sites routinely need to modify, please consider
+# putting that configuration into rpki.conf and just adding code here
+# to read that configuration.
+try:
+ from local_settings import *
+except:
+ pass
diff --git a/rpki/gui/models.py b/rpki/gui/models.py
new file mode 100644
index 00000000..7a684f32
--- /dev/null
+++ b/rpki/gui/models.py
@@ -0,0 +1,150 @@
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Common classes for reuse in apps.
+"""
+
+__version__ = '$Id$'
+
+from django.db import models
+
+import rpki.resource_set
+import rpki.POW
+from south.modelsinspector import add_introspection_rules
+
+
+class IPv6AddressField(models.Field):
+ "Field large enough to hold a 128-bit unsigned integer."
+
+ __metaclass__ = models.SubfieldBase
+
+ def db_type(self, connection):
+ return 'binary(16)'
+
+ def to_python(self, value):
+ if isinstance(value, rpki.POW.IPAddress):
+ return value
+ return rpki.POW.IPAddress.fromBytes(value)
+
+ def get_db_prep_value(self, value, connection, prepared):
+ """
+ Note that we add a custom conversion to encode long values as hex
+ strings in SQL statements. See settings.get_conv() for details.
+
+ """
+ return value.toBytes()
+
+
+class IPv4AddressField(models.Field):
+ "Wrapper around rpki.POW.IPAddress."
+
+ __metaclass__ = models.SubfieldBase
+
+ def db_type(self, connection):
+ return 'int UNSIGNED'
+
+ def to_python(self, value):
+ if isinstance(value, rpki.POW.IPAddress):
+ return value
+ return rpki.POW.IPAddress(value, version=4)
+
+ def get_db_prep_value(self, value, connection, prepared):
+ return long(value)
+
+add_introspection_rules(
+ [
+ ([IPv4AddressField, IPv6AddressField], [], {})
+ ],
+ ['^rpki\.gui\.models\.IPv4AddressField',
+ '^rpki\.gui\.models\.IPv6AddressField']
+)
+
+
+class Prefix(models.Model):
+ """Common implementation for models with an IP address range.
+
+ Expects that `range_cls` is set to the appropriate subclass of
+ rpki.resource_set.resource_range_ip."""
+
+ def as_resource_range(self):
+ """
+ Returns the prefix as a rpki.resource_set.resource_range_ip object.
+ """
+ return self.range_cls(self.prefix_min, self.prefix_max)
+
+ @property
+ def prefixlen(self):
+ "Returns the prefix length for the prefix in this object."
+ return self.as_resource_range().prefixlen()
+
+ def get_prefix_display(self):
+ "Return a string representatation of this IP prefix."
+ return str(self.as_resource_range())
+
+ def __unicode__(self):
+ """This method may be overridden by subclasses. The default
+ implementation calls get_prefix_display(). """
+ return self.get_prefix_display()
+
+ class Meta:
+ abstract = True
+
+ # default sort order reflects what "sh ip bgp" outputs
+ ordering = ('prefix_min',)
+
+
+class PrefixV4(Prefix):
+ "IPv4 Prefix."
+
+ range_cls = rpki.resource_set.resource_range_ipv4
+
+ prefix_min = IPv4AddressField(db_index=True, null=False)
+ prefix_max = IPv4AddressField(db_index=True, null=False)
+
+ class Meta(Prefix.Meta):
+ abstract = True
+
+
+class PrefixV6(Prefix):
+ "IPv6 Prefix."
+
+ range_cls = rpki.resource_set.resource_range_ipv6
+
+ prefix_min = IPv6AddressField(db_index=True, null=False)
+ prefix_max = IPv6AddressField(db_index=True, null=False)
+
+ class Meta(Prefix.Meta):
+ abstract = True
+
+
+class ASN(models.Model):
+ """Represents a range of ASNs.
+
+ This model is abstract, and is intended to be reused by applications."""
+
+ min = models.PositiveIntegerField(null=False)
+ max = models.PositiveIntegerField(null=False)
+
+ class Meta:
+ abstract = True
+ ordering = ('min', 'max')
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_as(self.min, self.max)
+
+ def __unicode__(self):
+ return u'AS%s' % self.as_resource_range()
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpki/gui/routeview/__init__.py b/rpki/gui/routeview/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpki/gui/routeview/__init__.py
diff --git a/rpki/gui/routeview/api.py b/rpki/gui/routeview/api.py
new file mode 100644
index 00000000..cf699c9a
--- /dev/null
+++ b/rpki/gui/routeview/api.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+import json
+from django import http
+from rpki.gui.routeview.models import RouteOrigin, RouteOriginV6
+from rpki import resource_set
+import rpki.exceptions
+
+def route_list(request):
+ """Implements the REST query against the route models to allow the client
+ to search for routes.
+
+ The only search currently supported is returning all the routes covered by
+ the prefix given in the 'prefix__in=' query string parameter.
+
+ By default, only returns up to 10 matching routes, but the client may
+ request a different limit with the 'count=' query string parameter.
+
+ """
+ hard_limit = 100
+
+ if request.method == 'GET' and 'prefix__in' in request.GET:
+ # find all routers covered by this prefix
+ match_prefix = request.GET.get('prefix__in')
+ # max number of items to return
+ limit = request.GET.get('count', 10)
+ if limit < 1 or limit > hard_limit:
+ return http.HttpResponseBadRequest('invalid value for count parameter')
+
+ try:
+ if ':' in match_prefix:
+ # v6
+ pfx = resource_set.resource_range_ipv6.parse_str(match_prefix)
+ manager = RouteOriginV6
+ else:
+ # v4
+ pfx = resource_set.resource_range_ipv4.parse_str(match_prefix)
+ manager = RouteOrigin
+ except (AssertionError, rpki.exceptions.BadIPResource), e:
+ return http.HttpResponseBadRequest(e)
+
+ try:
+ qs = manager.objects.filter(prefix_min__gte=pfx.min,
+ prefix_max__lte=pfx.max)[:limit]
+ # FIXME - a REST API should really return the url of the resource,
+ # but since we are combining two separate tables, the .pk is not a
+ # unique identifier.
+ matches = [{'prefix': str(x.as_resource_range()), 'asn': x.asn} for x in qs]
+ except IndexError:
+ # no matches
+ matches = []
+
+ return http.HttpResponse(json.dumps(matches), content_type='text/javascript')
+
+ return http.HttpResponseBadRequest()
diff --git a/rpki/gui/routeview/models.py b/rpki/gui/routeview/models.py
new file mode 100644
index 00000000..052860c4
--- /dev/null
+++ b/rpki/gui/routeview/models.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.db.models import PositiveIntegerField, permalink
+import rpki.gui.models
+
+
+class RouteOrigin(rpki.gui.models.PrefixV4):
+ "Represents an IPv4 BGP routing table entry."
+
+ asn = PositiveIntegerField(help_text='origin AS', null=False)
+
+ def __unicode__(self):
+ return u"AS%d's route origin for %s" % (self.asn,
+ self.get_prefix_display())
+
+ @property
+ def roas(self):
+ "Return a queryset of ROAs which cover this route."
+ return rpki.gui.cacheview.models.ROA.objects.filter(
+ prefixes__prefix_min__lte=self.prefix_min,
+ prefixes__prefix_max__gte=self.prefix_max
+ )
+
+ @property
+ def roa_prefixes(self):
+ "Return a queryset of ROA prefixes which cover this route."
+ return rpki.gui.cacheview.models.ROAPrefixV4.objects.filter(
+ prefix_min__lte=self.prefix_min,
+ prefix_max__gte=self.prefix_max
+ )
+
+ @property
+ def status(self):
+ "Returns the validation status of this route origin object."
+ roas = self.roas
+ # subselect exact match
+ if self.asn != 0 and roas.filter(asid=self.asn, prefixes__max_length__gte=self.prefixlen).exists():
+ return 'valid'
+ elif roas.exists():
+ return 'invalid'
+ return 'unknown'
+
+ @permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.route_detail', [str(self.pk)])
+
+ class Meta:
+ # sort by increasing mask length (/16 before /24)
+ ordering = ('prefix_min', '-prefix_max')
+
+
+class RouteOriginV6(rpki.gui.models.PrefixV6):
+ "Represents an IPv6 BGP routing table entry."
+
+ asn = PositiveIntegerField(help_text='origin AS', null=False)
+
+ def __unicode__(self):
+ return u"AS%d's route origin for %s" % (self.asn,
+ self.get_prefix_display())
+
+ class Meta:
+ ordering = ('prefix_min', '-prefix_max')
+
+
+# this goes at the end of the file to avoid problems with circular imports
+import rpki.gui.cacheview.models
diff --git a/rpki/gui/routeview/util.py b/rpki/gui/routeview/util.py
new file mode 100644
index 00000000..7884224c
--- /dev/null
+++ b/rpki/gui/routeview/util.py
@@ -0,0 +1,236 @@
+# Copyright (C) 2012, 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+__all__ = ('import_routeviews_dump')
+
+import itertools
+import _mysql_exceptions
+import os.path
+import subprocess
+import time
+import logging
+import urlparse
+from urllib import urlretrieve, unquote
+
+from django.db import transaction, connection
+
+from rpki.resource_set import resource_range_ipv4, resource_range_ipv6
+from rpki.exceptions import BadIPResource
+import rpki.gui.app.timestamp
+
+# globals
+logger = logging.getLogger(__name__)
+
+# Eventually this can be retrived from rpki.conf
+DEFAULT_URL = 'http://archive.routeviews.org/oix-route-views/oix-full-snapshot-latest.dat.bz2'
+
+def parse_text(f):
+ last_prefix = None
+ cursor = connection.cursor()
+ range_class = resource_range_ipv4
+ table = 'routeview_routeorigin'
+ sql = "INSERT INTO %s_new SET asn=%%s, prefix_min=%%s, prefix_max=%%s" % table
+
+ try:
+ logger.info('Dropping existing staging table...')
+ cursor.execute('DROP TABLE IF EXISTS %s_new' % table)
+ except _mysql_exceptions.Warning:
+ pass
+
+ logger.info('Creating staging table...')
+ cursor.execute('CREATE TABLE %(table)s_new LIKE %(table)s' % {'table': table})
+
+ logger.info('Disabling autocommit...')
+ cursor.execute('SET autocommit=0')
+
+ logger.info('Adding rows to table...')
+ for row in itertools.islice(f, 5, None):
+ cols = row.split()
+
+ # index -1 is i/e/? for igp/egp
+ origin_as = cols[-2]
+ # FIXME: skip AS_SETs
+ if origin_as[0] == '{':
+ continue
+
+ prefix = cols[1]
+
+ # validate the prefix since the "sh ip bgp" output is sometimes
+ # corrupt by no space between the prefix and the next hop IP
+ # address.
+ net, bits = prefix.split('/')
+ if len(bits) > 2:
+ s = ['mask for %s looks fishy...' % prefix]
+ prefix = '%s/%s' % (net, bits[0:2])
+ s.append('assuming it should be %s' % prefix)
+ logger.warning(' '.join(s))
+
+ # the output may contain multiple paths to the same origin.
+ # if this is the same prefix as the last entry, we don't need
+ # to validate it again.
+ #
+ # prefixes are sorted, but the origin_as is not, so we keep a set to
+ # avoid duplicates, and insert into the db once we've seen all the
+ # origin_as values for a given prefix
+ if prefix != last_prefix:
+ # output routes for previous prefix
+ if last_prefix is not None:
+ try:
+ rng = range_class.parse_str(last_prefix)
+ rmin = long(rng.min)
+ rmax = long(rng.max)
+ cursor.executemany(sql, [(asn, rmin, rmax) for asn in asns])
+ except BadIPResource:
+ logger.warning('skipping bad prefix: ' + last_prefix)
+
+ asns = set()
+ last_prefix = prefix
+
+ try:
+ asns.add(int(origin_as))
+ except ValueError as err:
+ logger.warning('\n'.join(
+ ['unable to parse origin AS: ' + origin_as],
+ ['ValueError: ' + str(err)]
+ ['route entry was: ' + row],
+ ))
+
+ logger.info('Committing...')
+ cursor.execute('COMMIT')
+
+ try:
+ logger.info('Dropping old table...')
+ cursor.execute('DROP TABLE IF EXISTS %s_old' % table)
+ except _mysql_exceptions.Warning:
+ pass
+
+ logger.info('Swapping staging table with live table...')
+ cursor.execute('RENAME TABLE %(table)s TO %(table)s_old, %(table)s_new TO %(table)s' % {'table': table})
+
+ transaction.commit_unless_managed()
+
+ logger.info('Updating timestamp metadata...')
+ rpki.gui.app.timestamp.update('bgp_v4_import')
+
+
+def parse_mrt(f):
+ # filter input through bgpdump
+ pipe = subprocess.Popen(['bgpdump', '-m', '-v', '-'], stdin=f,
+ stdout=subprocess.PIPE)
+
+ last_prefix = None
+ last_as = None
+ for e in pipe.stdout.readlines():
+ a = e.split('|')
+ prefix = a[5]
+ try:
+ origin_as = int(a[6].split()[-1])
+ except ValueError:
+ # skip AS_SETs
+ continue
+
+ if prefix != last_prefix:
+ last_prefix = prefix
+ elif last_as == origin_as:
+ continue
+ last_as = origin_as
+
+ asns = PREFIXES.get(prefix)
+ if not asns:
+ asns = set()
+ PREFIXES[prefix] = asns
+ asns.add(origin_as)
+
+ pipe.wait()
+ if pipe.returncode:
+ raise ProgException('bgpdump exited with code %d' % pipe.returncode)
+
+
+class ProgException(Exception):
+ pass
+
+
+class UnknownInputType(ProgException):
+ pass
+
+
+class PipeFailed(ProgException):
+ pass
+
+
+def import_routeviews_dump(filename=DEFAULT_URL, filetype='auto'):
+ """Load the oix-full-snapshot-latest.bz2 from routeview.org into the
+ rpki.gui.routeview database.
+
+ Arguments:
+
+ filename [optional]: the full path to the downloaded file to parse
+
+ filetype [optional]: 'text' or 'mrt'
+
+ """
+ start_time = time.time()
+
+ if filename.startswith('http://'):
+ #get filename from the basename of the URL
+ u = urlparse.urlparse(filename)
+ bname = os.path.basename(unquote(u.path))
+ tmpname = os.path.join('/tmp', bname)
+
+ logger.info("Downloading %s to %s" % (filename, tmpname))
+ if os.path.exists(tmpname):
+ os.remove(tmpname)
+ # filename is replaced with a local filename containing cached copy of
+ # URL
+ filename, headers = urlretrieve(filename, tmpname)
+
+ if filetype == 'auto':
+ # try to determine input type from filename, based on the default
+ # filenames from archive.routeviews.org
+ bname = os.path.basename(filename)
+ if bname.startswith('oix-full-snapshot-latest'):
+ filetype = 'text'
+ elif bname.startswith('rib.'):
+ filetype = 'mrt'
+ else:
+ raise UnknownInputType('unable to automatically determine input file type')
+ logging.info('Detected import format as "%s"' % filetype)
+
+ pipe = None
+ if filename.endswith('.bz2'):
+ bunzip = 'bunzip2'
+ logging.info('Decompressing input file on the fly...')
+ pipe = subprocess.Popen([bunzip, '--stdout', filename],
+ stdout=subprocess.PIPE)
+ input_file = pipe.stdout
+ else:
+ input_file = open(filename)
+
+ try:
+ dispatch = {'text': parse_text, 'mrt': parse_mrt}
+ dispatch[filetype](input_file)
+ except KeyError:
+ raise UnknownInputType('"%s" is an unknown input file type' % filetype)
+
+ if pipe:
+ logging.debug('Waiting for child to exit...')
+ pipe.wait()
+ if pipe.returncode:
+ raise PipeFailed('Child exited code %d' % pipe.returncode)
+ pipe = None
+ else:
+ input_file.close()
+
+ logger.info('Elapsed time %d secs' % (time.time() - start_time))
diff --git a/rpki/gui/script_util.py b/rpki/gui/script_util.py
new file mode 100644
index 00000000..c3a864fd
--- /dev/null
+++ b/rpki/gui/script_util.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+"""
+This module contains utility functions for use in standalone scripts.
+"""
+
+from django.conf import settings
+
+from rpki import config
+from rpki import autoconf
+
+__version__ = '$Id$'
+
+
+def setup():
+ """
+ Configure Django enough to use the ORM.
+ """
+ cfg = config.parser(section='web_portal')
+ # INSTALLED_APPS doesn't seem necessary so long as you are only accessing
+ # existing tables.
+ settings.configure(
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': cfg.get('sql-database'),
+ 'USER': cfg.get('sql-username'),
+ 'PASSWORD': cfg.get('sql-password'),
+ }
+ },
+ )
diff --git a/rpki/gui/urls.py b/rpki/gui/urls.py
new file mode 100644
index 00000000..955092f5
--- /dev/null
+++ b/rpki/gui/urls.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012, 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+from django.conf.urls import patterns, include
+
+urlpatterns = patterns(
+ '',
+
+ # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
+ # to INSTALLED_APPS to enable admin documentation:
+ #(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+ # Uncomment the next line to enable the admin:
+ #(r'^admin/', include(admin.site.urls)),
+
+ (r'^api/', include('rpki.gui.api.urls')),
+ (r'^cacheview/', include('rpki.gui.cacheview.urls')),
+ (r'^rpki/', include('rpki.gui.app.urls')),
+
+ (r'^accounts/login/$', 'rpki.gui.views.login'),
+ (r'^accounts/logout/$', 'rpki.gui.views.logout', {'next_page': '/rpki/'}),
+)
diff --git a/rpki/gui/views.py b/rpki/gui/views.py
new file mode 100644
index 00000000..404d6c7e
--- /dev/null
+++ b/rpki/gui/views.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2013 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+import django.contrib.auth.views
+from rpki.gui.decorators import tls_required
+
+
+@tls_required
+def login(request, *args, **kwargs):
+ "Wrapper around django.contrib.auth.views.login to force use of TLS."
+ return django.contrib.auth.views.login(request, *args, **kwargs)
+
+
+@tls_required
+def logout(request, *args, **kwargs):
+ "Wrapper around django.contrib.auth.views.logout to force use of TLS."
+ return django.contrib.auth.views.logout(request, *args, **kwargs)