summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/Server/Plugin.py4
-rw-r--r--src/lib/Server/Reports/reports/models.py120
-rw-r--r--src/lib/Server/Reports/reports/models.py.orig330
-rw-r--r--src/lib/Server/Reports/reports/models.py.rej15
-rw-r--r--src/lib/Server/Reports/reports/templates/404.html8
-rw-r--r--src/lib/Server/Reports/reports/templates/base-timeview.html25
-rw-r--r--src/lib/Server/Reports/reports/templates/base.html122
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/client-nodebox.html63
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/detail.html130
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/detailed-list.html79
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/history.html20
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/index.html61
-rw-r--r--src/lib/Server/Reports/reports/templates/clients/manage.html62
-rw-r--r--src/lib/Server/Reports/reports/templates/config_items/index.html100
-rw-r--r--src/lib/Server/Reports/reports/templates/config_items/item.html109
-rw-r--r--src/lib/Server/Reports/reports/templates/config_items/listing.html62
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/index.html18
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html7
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/summary-block.html90
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/summary.html63
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/sys_view.html20
-rw-r--r--src/lib/Server/Reports/reports/templates/displays/timing.html72
-rw-r--r--src/lib/Server/Reports/reports/templates/index.html15
-rw-r--r--src/lib/Server/Reports/reports/templates/widgets/filter_bar.html13
-rw-r--r--src/lib/Server/Reports/reports/templates/widgets/interaction_list.inc38
-rw-r--r--src/lib/Server/Reports/reports/templates/widgets/page_bar.html23
-rw-r--r--src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py239
-rw-r--r--src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py41
-rw-r--r--src/lib/Server/Reports/reports/templatetags/syntax_coloring.py26
-rw-r--r--src/lib/Server/Reports/reports/urls.py55
-rw-r--r--src/lib/Server/Reports/reports/views.py647
-rw-r--r--src/lib/Server/Reports/settings.py25
-rw-r--r--src/lib/Server/Reports/urls.py56
-rwxr-xr-xsrc/lib/Server/Reports/utils.py94
34 files changed, 1773 insertions, 1079 deletions
diff --git a/src/lib/Server/Plugin.py b/src/lib/Server/Plugin.py
index 95569e3ac..0458a4ce0 100644
--- a/src/lib/Server/Plugin.py
+++ b/src/lib/Server/Plugin.py
@@ -33,8 +33,8 @@ info_regex = re.compile( \
'encoding:(\s)*(?P<encoding>\w+)|' +
'group:(\s)*(?P<group>\S+)|' +
'important:(\s)*(?P<important>\S+)|' +
- 'mtime:(\s)*(?P<mtime>\w+)|' +
- 'owner:(\s)*(?P<owner>\S+)|' +
+ 'mtime:(\s)*(?P<mtime>\w+)$' +
+ '^owner:(\s)*(?P<owner>\S+)|' +
'paranoid:(\s)*(?P<paranoid>\S+)|' +
'perms:(\s)*(?P<perms>\w+)|')
diff --git a/src/lib/Server/Reports/reports/models.py b/src/lib/Server/Reports/reports/models.py
index 5468420f6..1963a9090 100644
--- a/src/lib/Server/Reports/reports/models.py
+++ b/src/lib/Server/Reports/reports/models.py
@@ -28,26 +28,30 @@ TYPE_CHOICES = (
(TYPE_MODIFIED, 'Modified'),
(TYPE_EXTRA, 'Extra'),
)
+
+def convert_entry_type_to_id(type_name):
+ """Convert a entry type to its entry id"""
+ for e_id, e_name in TYPE_CHOICES:
+ if e_name.lower() == type_name.lower():
+ return e_id
+ return -1
+
class ClientManager(models.Manager):
"""Extended client manager functions."""
- def active(self, timestamp='now'):
- '''returns a set of clients that have been created and have not yet been
+ def active(self, timestamp=None):
+ """returns a set of clients that have been created and have not yet been
expired as of optional timestmamp argument. Timestamp should be a
- string formatted in the fashion: 2006-01-01 00:00:00'''
+ datetime object."""
- if timestamp == 'now':
+ if timestamp == None:
timestamp = datetime.now()
+ elif not isinstance(timestamp, datetime):
+ raise ValueError, 'Expected a datetime object'
else:
- print timestamp
try:
timestamp = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6])
except ValueError:
- return self.filter(expiration__lt=timestamp, creation__gt=timestamp);
- '''
- - this is a really hacky way to return an empty QuerySet
- - this should return Client.objects.none() in Django
- development version.
- '''
+ return self.none()
return self.filter(Q(expiration__gt=timestamp) | Q(expiration__isnull=True),
creation__lt=timestamp)
@@ -81,33 +85,64 @@ class Ping(models.Model):
get_latest_by = 'endtime'
class InteractiveManager(models.Manager):
- """Manages interactions objects.
+ """Manages interactions objects."""
+
+ def recent_interactions_dict(self, maxdate=None, active_only=True):
+ """
+ Return the most recent interactions for clients as of a date.
+
+ This method uses aggregated queries to return a ValuesQueryDict object.
+ Faster then raw sql since this is executed as a single query.
+ """
- Returns most recent interaction as of specified timestamp in format:
- '2006-01-01 00:00:00' or 'now' or None->'now'
+ return self.values('client').annotate(max_timestamp=Max('timestamp')).values()
+
+ def interaction_per_client(self, maxdate = None, active_only=True):
+ """
+ Returns the most recent interactions for clients as of a date
+
+ Arguments:
+ maxdate -- datetime object. Most recent date to pull. (dafault None)
+ active_only -- Include only active clients (default True)
+
+ """
- """
- def interaction_per_client(self, maxdate = None):
- """Returns the most recent interactions for clients as of a date.
- FIXME - check the dates passed in.
+ if maxdate and not isinstance(maxdate,datetime):
+ raise ValueError, 'Expected a datetime object'
+ return self.filter(id__in = self.get_interaction_per_client_ids(maxdate, active_only))
+
+ def get_interaction_per_client_ids(self, maxdate = None, active_only=True):
+ """
+ Returns the ids of most recent interactions for clients as of a date.
+
+ Arguments:
+ maxdate -- datetime object. Most recent date to pull. (dafault None)
+ active_only -- Include only active clients (default True)
"""
from django.db import connection
cursor = connection.cursor()
+ cfilter = "expiration is null"
sql = 'select reports_interaction.id, x.client_id from (select client_id, MAX(timestamp) ' + \
'as timer from reports_interaction'
- if maxdate != 'now':
- sql = sql + " where timestamp < '%s' " % maxdate
+ if maxdate:
+ if not isinstance(maxdate,datetime):
+ raise ValueError, 'Expected a datetime object'
+ sql = sql + " where timestamp <= '%s' " % maxdate
+ cfilter = "(expiration is null or expiration > '%s') and creation <= '%s'" % (maxdate,maxdate)
sql = sql + ' GROUP BY client_id) x, reports_interaction where ' + \
'reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer'
+ if active_only:
+ sql = sql + " and x.client_id in (select id from reports_client where %s)" % \
+ cfilter
try:
cursor.execute(sql)
+ return [item[0] for item in cursor.fetchall()]
except:
'''FIXME - really need some error hadling'''
- return self.none()
- return self.filter(id__in = [item[0] for item in cursor.fetchall()])
-
+ pass
+ return []
class Interaction(models.Model):
"""Models each reconfiguration operation interaction between client and server."""
@@ -213,6 +248,7 @@ class Interaction(models.Model):
pass
class Meta:
get_latest_by = 'timestamp'
+ ordering = ['-timestamp']
unique_together = ("client", "timestamp")
class Reason(models.Model):
@@ -267,42 +303,6 @@ class Entries_interactions(models.Model):
interaction = models.ForeignKey(Interaction)
type = models.IntegerField(choices=TYPE_CHOICES)
-class PerformanceManager(models.Manager):
- """
- Provides ability to effectively query for performance information
- It is possible this should move to the view
-
- """
- #Date format for maxdate: '2006-01-01 00:00:00'
- def performance_per_client(self, maxdate = None):
- from django.db import connection
- cursor = connection.cursor()
- if (maxdate == 'now' or maxdate == None):
- cursor.execute("SELECT reports_client.name, reports_performance.metric, reports_performance.value "+
- "FROM reports_performance, reports_performance_interaction, reports_client WHERE ( "+
- "reports_client.current_interaction_id = reports_performance_interaction.interaction_id AND "+
- "reports_performance.id = reports_performance_interaction.performance_id)")
- else:
- cursor.execute("select reports_client.name, reports_performance.metric, "+
- "reports_performance.value from (Select reports_interaction.client_id as client_id, "+
- "MAX(reports_interaction.timestamp) as timestamp from reports_interaction where "+
- "timestamp < %s GROUP BY reports_interaction.client_id) x, reports_client, "+
- "reports_interaction, reports_performance, reports_performance_interaction where "+
- "reports_client.id = x.client_id AND x.timestamp = reports_interaction.timestamp AND "+
- "x.client_id = reports_interaction.client_id AND reports_performance.id = "+
- "reports_performance_interaction.performance_id AND "+
- "reports_performance_interaction.interaction_id = reports_interaction.id", [maxdate])
-
- results = {}
- for row in cursor.fetchall():
- try:
- results[row[0]].__setitem__(row[1], row[2])
- except KeyError:
- results[row[0]] = {row[1]:row[2]}
-
- return results
-
-#performance metrics, models a performance-metric-item
class Performance(models.Model):
"""Object representing performance data for any interaction."""
interaction = models.ManyToManyField(Interaction, related_name="performance_items")
@@ -319,8 +319,6 @@ class Performance(models.Model):
cursor.execute('delete from reports_performance where not exists (select ri.id from reports_performance_interaction ri where ri.performance_id = reports_performance.id)')
transaction.set_dirty()
- objects = PerformanceManager()
-
class InternalDatabaseVersion(models.Model):
"""Object that tell us to witch version is the database."""
version = models.IntegerField()
diff --git a/src/lib/Server/Reports/reports/models.py.orig b/src/lib/Server/Reports/reports/models.py.orig
new file mode 100644
index 000000000..5468420f6
--- /dev/null
+++ b/src/lib/Server/Reports/reports/models.py.orig
@@ -0,0 +1,330 @@
+"""Django models for Bcfg2 reports."""
+from django.db import models
+from django.db import connection, transaction
+from django.db.models import Q
+from datetime import datetime, timedelta
+from time import strptime
+
+KIND_CHOICES = (
+ #These are the kinds of config elements
+ ('Package', 'Package'),
+ ('Path', 'directory'),
+ ('Path', 'file'),
+ ('Path', 'permissions'),
+ ('Path', 'symlink'),
+ ('Service', 'Service'),
+)
+PING_CHOICES = (
+ #These are possible ping states
+ ('Up (Y)', 'Y'),
+ ('Down (N)', 'N')
+)
+TYPE_BAD = 1
+TYPE_MODIFIED = 2
+TYPE_EXTRA = 3
+
+TYPE_CHOICES = (
+ (TYPE_BAD, 'Bad'),
+ (TYPE_MODIFIED, 'Modified'),
+ (TYPE_EXTRA, 'Extra'),
+)
+class ClientManager(models.Manager):
+ """Extended client manager functions."""
+ def active(self, timestamp='now'):
+ '''returns a set of clients that have been created and have not yet been
+ expired as of optional timestmamp argument. Timestamp should be a
+ string formatted in the fashion: 2006-01-01 00:00:00'''
+
+ if timestamp == 'now':
+ timestamp = datetime.now()
+ else:
+ print timestamp
+ try:
+ timestamp = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6])
+ except ValueError:
+ return self.filter(expiration__lt=timestamp, creation__gt=timestamp);
+ '''
+ - this is a really hacky way to return an empty QuerySet
+ - this should return Client.objects.none() in Django
+ development version.
+ '''
+
+ return self.filter(Q(expiration__gt=timestamp) | Q(expiration__isnull=True),
+ creation__lt=timestamp)
+
+
+class Client(models.Model):
+ """Object representing every client we have seen stats for."""
+ creation = models.DateTimeField(auto_now_add=True)
+ name = models.CharField(max_length=128,)
+ current_interaction = models.ForeignKey('Interaction',
+ null=True, blank=True,
+ related_name="parent_client")
+ expiration = models.DateTimeField(blank=True, null=True)
+
+ def __str__(self):
+ return self.name
+
+ objects = ClientManager()
+
+ class Admin:
+ pass
+
+class Ping(models.Model):
+ """Represents a ping of a client (sparsely)."""
+ client = models.ForeignKey(Client, related_name="pings")
+ starttime = models.DateTimeField()
+ endtime = models.DateTimeField()
+ status = models.CharField(max_length=4, choices=PING_CHOICES)#up/down
+
+ class Meta:
+ get_latest_by = 'endtime'
+
+class InteractiveManager(models.Manager):
+ """Manages interactions objects.
+
+ Returns most recent interaction as of specified timestamp in format:
+ '2006-01-01 00:00:00' or 'now' or None->'now'
+
+ """
+ def interaction_per_client(self, maxdate = None):
+ """Returns the most recent interactions for clients as of a date.
+ FIXME - check the dates passed in.
+
+ """
+ from django.db import connection
+ cursor = connection.cursor()
+
+ sql = 'select reports_interaction.id, x.client_id from (select client_id, MAX(timestamp) ' + \
+ 'as timer from reports_interaction'
+ if maxdate != 'now':
+ sql = sql + " where timestamp < '%s' " % maxdate
+ sql = sql + ' GROUP BY client_id) x, reports_interaction where ' + \
+ 'reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer'
+ try:
+ cursor.execute(sql)
+ except:
+ '''FIXME - really need some error hadling'''
+ return self.none()
+ return self.filter(id__in = [item[0] for item in cursor.fetchall()])
+
+
+class Interaction(models.Model):
+ """Models each reconfiguration operation interaction between client and server."""
+ client = models.ForeignKey(Client, related_name="interactions",)
+ timestamp = models.DateTimeField()#Timestamp for this record
+ state = models.CharField(max_length=32)#good/bad/modified/etc
+ repo_rev_code = models.CharField(max_length=64)#repo revision at time of interaction
+ client_version = models.CharField(max_length=32)#Client Version
+ goodcount = models.IntegerField()#of good config-items
+ totalcount = models.IntegerField()#of total config-items
+ server = models.CharField(max_length=256) # Name of the server used for the interaction
+ bad_entries = models.IntegerField(default=-1)
+ modified_entries = models.IntegerField(default=-1)
+ extra_entries = models.IntegerField(default=-1)
+
+ def __str__(self):
+ return "With " + self.client.name + " @ " + self.timestamp.isoformat()
+
+ def percentgood(self):
+ if not self.totalcount == 0:
+ return (self.goodcount/float(self.totalcount))*100
+ else:
+ return 0
+
+ def percentbad(self):
+ if not self.totalcount == 0:
+ return ((self.totalcount-self.goodcount)/(float(self.totalcount)))*100
+ else:
+ return 0
+
+ def isclean(self):
+ if (self.bad_entry_count() == 0 and self.goodcount == self.totalcount):
+ return True
+ else:
+ return False
+
+ def isstale(self):
+ if (self == self.client.current_interaction):#Is Mostrecent
+ if(datetime.now()-self.timestamp > timedelta(hours=25) ):
+ return True
+ else:
+ return False
+ else:
+ #Search for subsequent Interaction for this client
+ #Check if it happened more than 25 hrs ago.
+ if (self.client.interactions.filter(timestamp__gt=self.timestamp)
+ .order_by('timestamp')[0].timestamp -
+ self.timestamp > timedelta(hours=25)):
+ return True
+ else:
+ return False
+ def save(self):
+ super(Interaction, self).save() #call the real save...
+ self.client.current_interaction = self.client.interactions.latest()
+ self.client.save()#save again post update
+
+ def delete(self):
+ '''Override the default delete. Allows us to remove Performance items'''
+ pitems = list(self.performance_items.all())
+ super(Interaction, self).delete()
+ for perf in pitems:
+ if perf.interaction.count() == 0:
+ perf.delete()
+
+ def badcount(self):
+ return self.totalcount - self.goodcount
+
+ def bad(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_BAD)
+
+ def bad_entry_count(self):
+ """Number of bad entries. Store the count in the interation field to save db queries."""
+ if self.bad_entries < 0:
+ self.bad_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_BAD).count()
+ self.save()
+ return self.bad_entries
+
+ def modified(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_MODIFIED)
+
+ def modified_entry_count(self):
+ """Number of modified entries. Store the count in the interation field to save db queries."""
+ if self.modified_entries < 0:
+ self.modified_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_MODIFIED).count()
+ self.save()
+ return self.modified_entries
+
+ def extra(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_EXTRA)
+
+ def extra_entry_count(self):
+ """Number of extra entries. Store the count in the interation field to save db queries."""
+ if self.extra_entries < 0:
+ self.extra_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_EXTRA).count()
+ self.save()
+ return self.extra_entries
+
+ objects = InteractiveManager()
+
+ class Admin:
+ list_display = ('client', 'timestamp', 'state')
+ list_filter = ['client', 'timestamp']
+ pass
+ class Meta:
+ get_latest_by = 'timestamp'
+ unique_together = ("client", "timestamp")
+
+class Reason(models.Model):
+ """reason why modified or bad entry did not verify, or changed."""
+ owner = models.TextField(max_length=128, blank=True)
+ current_owner = models.TextField(max_length=128, blank=True)
+ group = models.TextField(max_length=128, blank=True)
+ current_group = models.TextField(max_length=128, blank=True)
+ perms = models.TextField(max_length=4, blank=True)#txt fixes typing issue
+ current_perms = models.TextField(max_length=4, blank=True)
+ status = models.TextField(max_length=3, blank=True)#on/off/(None)
+ current_status = models.TextField(max_length=1, blank=True)#on/off/(None)
+ to = models.TextField(max_length=256, blank=True)
+ current_to = models.TextField(max_length=256, blank=True)
+ version = models.TextField(max_length=128, blank=True)
+ current_version = models.TextField(max_length=128, blank=True)
+ current_exists = models.BooleanField()#False means its missing. Default True
+ current_diff = models.TextField(max_length=1280, blank=True)
+ is_binary = models.BooleanField(default=False)
+ def _str_(self):
+ return "Reason"
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ cursor.execute('delete from reports_reason where not exists (select rei.id from reports_entries_interactions rei where rei.reason_id = reports_reason.id)')
+ transaction.set_dirty()
+
+
+class Entries(models.Model):
+ """Contains all the entries feed by the client."""
+ name = models.CharField(max_length=128, db_index=True)
+ kind = models.CharField(max_length=16, choices=KIND_CHOICES, db_index=True)
+
+ def __str__(self):
+ return self.name
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ cursor.execute('delete from reports_entries where not exists (select rei.id from reports_entries_interactions rei where rei.entry_id = reports_entries.id)')
+ transaction.set_dirty()
+
+class Entries_interactions(models.Model):
+ """Define the relation between the reason, the interaction and the entry."""
+ entry = models.ForeignKey(Entries)
+ reason = models.ForeignKey(Reason)
+ interaction = models.ForeignKey(Interaction)
+ type = models.IntegerField(choices=TYPE_CHOICES)
+
+class PerformanceManager(models.Manager):
+ """
+ Provides ability to effectively query for performance information
+ It is possible this should move to the view
+
+ """
+ #Date format for maxdate: '2006-01-01 00:00:00'
+ def performance_per_client(self, maxdate = None):
+ from django.db import connection
+ cursor = connection.cursor()
+ if (maxdate == 'now' or maxdate == None):
+ cursor.execute("SELECT reports_client.name, reports_performance.metric, reports_performance.value "+
+ "FROM reports_performance, reports_performance_interaction, reports_client WHERE ( "+
+ "reports_client.current_interaction_id = reports_performance_interaction.interaction_id AND "+
+ "reports_performance.id = reports_performance_interaction.performance_id)")
+ else:
+ cursor.execute("select reports_client.name, reports_performance.metric, "+
+ "reports_performance.value from (Select reports_interaction.client_id as client_id, "+
+ "MAX(reports_interaction.timestamp) as timestamp from reports_interaction where "+
+ "timestamp < %s GROUP BY reports_interaction.client_id) x, reports_client, "+
+ "reports_interaction, reports_performance, reports_performance_interaction where "+
+ "reports_client.id = x.client_id AND x.timestamp = reports_interaction.timestamp AND "+
+ "x.client_id = reports_interaction.client_id AND reports_performance.id = "+
+ "reports_performance_interaction.performance_id AND "+
+ "reports_performance_interaction.interaction_id = reports_interaction.id", [maxdate])
+
+ results = {}
+ for row in cursor.fetchall():
+ try:
+ results[row[0]].__setitem__(row[1], row[2])
+ except KeyError:
+ results[row[0]] = {row[1]:row[2]}
+
+ return results
+
+#performance metrics, models a performance-metric-item
+class Performance(models.Model):
+ """Object representing performance data for any interaction."""
+ interaction = models.ManyToManyField(Interaction, related_name="performance_items")
+ metric = models.CharField(max_length=128)
+ value = models.DecimalField(max_digits=32, decimal_places=16)
+ def __str__(self):
+ return self.metric
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ cursor.execute('delete from reports_performance where not exists (select ri.id from reports_performance_interaction ri where ri.performance_id = reports_performance.id)')
+ transaction.set_dirty()
+
+ objects = PerformanceManager()
+
+class InternalDatabaseVersion(models.Model):
+ """Object that tell us to witch version is the database."""
+ version = models.IntegerField()
+ updated = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return "version %d updated the %s" % (self.version, self.updated.isoformat())
diff --git a/src/lib/Server/Reports/reports/models.py.rej b/src/lib/Server/Reports/reports/models.py.rej
new file mode 100644
index 000000000..c8e1b2229
--- /dev/null
+++ b/src/lib/Server/Reports/reports/models.py.rej
@@ -0,0 +1,15 @@
+***************
+*** 1,6 ****
+ """Django models for Bcfg2 reports."""
+ from django.db import models
+- from django.db.models import Q
+ from datetime import datetime, timedelta
+ from time import strptime
+
+--- 1,6 ----
+ """Django models for Bcfg2 reports."""
+ from django.db import models
++ from django.db.models import Q, Max
+ from datetime import datetime, timedelta
+ from time import strptime
+
diff --git a/src/lib/Server/Reports/reports/templates/404.html b/src/lib/Server/Reports/reports/templates/404.html
new file mode 100644
index 000000000..168bd9fec
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/404.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% block title %}Bcfg2 - Page not found{% endblock %}
+{% block fullcontent %}
+<h2>Page not found</h2>
+<p>
+The page or object requested could not be found.
+</p>
+{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/base-timeview.html b/src/lib/Server/Reports/reports/templates/base-timeview.html
new file mode 100644
index 000000000..d0617cde7
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/base-timeview.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block timepiece %}
+<script type="text/javascript">
+function showCalendar() {
+ var cal = new CalendarPopup("calendar_div");
+ cal.showYearNavigation();
+ cal.select(document.forms['cal_form'].cal_date,'cal_link',
+ 'yyyy/MM/dd' {% if timestamp %}, '{{ timestamp|date:'Y/m/d' }}'{% endif %} );
+ return false;
+}
+function bcfg2_check_date() {
+ var new_date = document.getElementById('cal_date').value;
+ if(new_date) {
+ document.cal_form.submit();
+ }
+}
+document.write(getCalendarStyles());
+</script>
+{% if not timestamp %}Rendered at {% now "Y-m-d H:i" %} | {% else %}View as of {{ timestamp|date:"Y-m-d H:i" }} | {% endif %}{% spaceless %}
+ <a id='cal_link' name='cal_link' href='#' onclick='showCalendar(); return false;'
+ >[change]</a>
+ <form method='post' action='{{ path }}' id='cal_form' name='cal_form'><input id='cal_date' name='cal_date' type='hidden' value=''/></form>
+{% endspaceless %}
+{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/base.html b/src/lib/Server/Reports/reports/templates/base.html
index d42e26960..64c105e34 100644
--- a/src/lib/Server/Reports/reports/templates/base.html
+++ b/src/lib/Server/Reports/reports/templates/base.html
@@ -1,55 +1,93 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
- <title>{% block title %}Bcfg2 Reporting System{% endblock %}</title>
- <link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/boxypastel.css" />
- <link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/base.css" />
- <script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/main.js"></script>
- {% block extra_header_info %}{% endblock %}
+<title>{% block title %}Bcfg2 Reporting System{% endblock %}</title>
+
+<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+<meta http-equiv="Content-language" content="en" />
+<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
+<meta name="robots" content="noindex, nofollow" />
+<meta http-equiv="cache-control" content="no-cache" />
+
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}/bcfg2_base.css" media="all" />
+<script type="text/javascript" src="{{ MEDIA_URL }}/bcfg2.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}/date.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}/AnchorPosition.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}/CalendarPopup.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}/PopupWindow.js"></script>
+{% block extra_header_info %}{% endblock %}
+
</head>
+<body onload="{% block body_onload %}{% endblock %}">
-<body>
<div id="header">
- <div id="branding">
- <h1>Bcfg2 Reporting System</h1>
+ <a href="http://trac.mcs.anl.gov/projects/bcfg2"><img src='{{ MEDIA_URL }}/bcfg2_logo.png'
+ height='115' width='300' alt='Bcfg2' style='float:left; height: 115px' /></a>
</div>
- <div id="user-tools">...Change is Coming...</div>
+
+<div id="document">
+ <div id="content"><div id="contentwrapper">
+ {% block fullcontent %}
+ <div class='page_name'>
+ <h1>{% block pagebanner %}Page Banner{% endblock %}</h1>
+ <div id="timepiece">{% block timepiece %}Rendered at {% now "Y-m-d H:i" %}{% endblock %}</div>
</div>
- <div id="content-main">
- <div id="sidebar">
- {% block sidebar %}
- <ul class="sidebar">
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../" class="sidebar">Home</a></li>
- <li>
- <a href="{% url Bcfg2.Server.Reports.reports.views.client_index %}" class="sidebar">Clients</a>
- <ul class="sidebar-level2">
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.client_detailed_list %}" class="sidebar">Detailed List</a></li>
- </ul>
- </li>
- <li>
- <a href="{% url Bcfg2.Server.Reports.reports.views.display_index %}" class="sidebar">Displays</a>
- <ul class="sidebar-level2">
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_sys_view %}" class="sidebar">System</a></li>
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_summary %}" class="sidebar">Summary</a></li>
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_timing %}" class="sidebar">Timing</a></li>
- </ul>
- </li>
- <li>
- <span class="sidebar">Config Items</span>
- <ul class="sidebar-level2">
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.bad_item_index %}" class="sidebar">Bad</a></li>
- <li><a href="{% url Bcfg2.Server.Reports.reports.views.modified_item_index %}" class="sidebar">Modified</a></li>
- </ul>
- </li>
+ <div class='detail_wrapper'>
+ {% block content %}{% endblock %}
+ </div>
+ {% endblock %}
+ </div></div><!-- content -->
+ <div id="sidemenucontainer"><div id="sidemenu">
+ {% block sidemenu %}
+ <ul class='menu-level1'>
+ <li>Overview</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="{% url reports_summary %}">Summary</a></li>
+ <li><a href="{% url reports_history %}">Recent Interactions</a></li>
+ <li><a href="{% url reports_timing %}">Timing</a></li>
+ </ul>
+ <ul class='menu-level1'>
+ <li>Clients</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="{% url reports_grid_view %}">Grid View</a></li>
+ <li><a href="{% url reports_detailed_list %}">Detailed List</a></li>
+ <li><a href="{% url reports_client_manage %}">Manage</a></li>
+ </ul>
+ <ul class='menu-level1'>
+ <li>Entries Configured</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="{% url reports_item_list "bad" %}">Bad</a></li>
+ <li><a href="{% url reports_item_list "modified" %}">Modified</a></li>
+ <li><a href="{% url reports_item_list "extra" %}">Extra</a></li>
+ </ul>
+{% comment %}
+ TODO
+ <ul class='menu-level1'>
+ <li>Entry Types</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Package</a></li>
+ <li><a href="#">Path</a></li>
+ <li><a href="#">Service</a></li>
+ </ul>
+{% endcomment %}
+ <ul class='menu-level1'>
+ <li><a href="http://trac.mcs.anl.gov/projects/bcfg2">Homepage</a></li>
+ <li><a href="http://doc.bcfg2.fourkitchens.com/index.html">Documentation</a></li>
</ul>
{% endblock %}
+ </div></div><!-- sidemenu -->
+ <div style='clear:both'></div>
+</div><!-- document -->
+ <div id="footer">
+ <span>Bcfg2 Version 1.1.0</span>
</div>
- <div id="container">
- {% block pagebanner %}{% endblock %}
- {% block content %}{% endblock %}
- </div>
- </div>
+<div id="calendar_div" style='position:absolute; visibility:hidden; background-color:white; layer-background-color:white;'></div>
</body>
</html>
diff --git a/src/lib/Server/Reports/reports/templates/clients/client-nodebox.html b/src/lib/Server/Reports/reports/templates/clients/client-nodebox.html
deleted file mode 100644
index 8dbd01d9a..000000000
--- a/src/lib/Server/Reports/reports/templates/clients/client-nodebox.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% load django_templating_sigh %}
-{% if client %}
- <a name="{{client.name}}"></a>
- <div class="nodebox">
- <span class="notebox">Time Ran: {{interaction.timestamp}}</span>
- <!--<span class="configbox">(-Insert Profile Name Here-)</span>-->
-
- <table class="invisitable">
- <tr><td width="43%"><h2>Node: <span class="nodename">
- <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=client.name, pk=client.current_interaction.id %}">{{client.name}}</a></span></h2></td>
- <td width="23%">
- {% if interaction.repo_rev_code %}Revision: {{interaction.repo_rev_code}}{% endif %}
- </td>
- <td width="33%"><div class="statusborder">
- <div class="greenbar" style="width: {{interaction.percentgood}}%;">&nbsp;</div>
- <div class="redbar" style="width: {{interaction.percentbad}}%;">&nbsp;</div>
- </div>
- </td></tr>
- </table>
- {% if interaction.isclean %}
- <div class="clean">
- <span class="nodelisttitle">Node is clean; Everything has been satisfactorily configured.</span>
- </div>
- {% endif %}
- {% if interaction.isstale %}
- <div class="warning">
- <span class="nodelisttitle">This node did not run within the last 24 hours-- it may be out of date.</span>
- </div>
- {% endif %}
- {% if interaction.bad %}
- <div class="bad">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-bad');" title="Click to expand" class="commentLink">{{interaction.bad.count}}</a> items did not verify and are considered Dirty.<br /></span>
- <div class="items" id="{{client.name}}-bad"><ul class="plain">
- {% for bad in interaction.bad|sortwell %}
- <li><strong>{{bad.entry.kind}}: </strong><tt><a href="{% url Bcfg2.Server.Reports.reports.views.config_item_bad bad.id%}">{{bad.entry.name}}</a></tt></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if interaction.modified %}
- <div class="modified">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-modified');" title="Click to expand" class="commentLink">{{interaction.modified.count}}</a> items were modified in the last run.<br /></span>
- <div class="items" id="{{client.name}}-modified"><ul class="plain">
- {% for modified in interaction.modified|sortwell %}
- <li><strong>{{modified.entry.kind}}: </strong><tt><a href="{% url Bcfg2.Server.Reports.reports.views.config_item_modified modified.id %}">{{modified.entry.name}}</a></tt></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if interaction.extra %}
- <div class="extra">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-extra');" title="Click to expand" class="commentLink">{{interaction.extra.count}}</a> extra configuration elements on the node.<br /></span>
- <div class="items" id="{{client.name}}-extra"><ul class="plain">
- {% for extra in interaction.extra|sortwell %}
- <li><strong>{{extra.entry.kind}}: </strong><tt>{{extra.entry.name}}</tt></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- </div>
-{% else %}
- <p>No record could be found for this client.</p>
-{% endif %}
diff --git a/src/lib/Server/Reports/reports/templates/clients/detail.html b/src/lib/Server/Reports/reports/templates/clients/detail.html
index 77f505804..efd5f9e00 100644
--- a/src/lib/Server/Reports/reports/templates/clients/detail.html
+++ b/src/lib/Server/Reports/reports/templates/clients/detail.html
@@ -1,17 +1,127 @@
{% extends "base.html" %}
+{% load bcfg2_tags %}
-{% block title %}Info for: {{client.name}}{% endblock %}
+{% block title %}Bcfg2 - Client {{client.name}}{% endblock %}
+
+{% block extra_header_info %}
+<style type="text/css">
+.node_data {
+ border: 1px solid #98DBCC;
+ margin: 10px;
+ padding-left: 18px;
+}
+.node_data td {
+ padding: 1px 20px 1px 2px;
+}
+span.history_links {
+ font-size: 90%;
+ margin-left: 50px;
+}
+span.history_links a {
+ font-size: 90%;
+}
+</style>
+{% endblock %}
+
+{% block body_onload %}javascript:clientdetailload(){% endblock %}
+
+{% block pagebanner %}Client Details{% endblock %}
{% block content %}
-<h2>Client Status Detail page for {{client.name}}</h2><br/>
-<b>Select time: </b>
-<select name=quick onChange="MM_jumpMenu('parent',this,0)">
- {% for i in client.interactions.all %}
- <option {% ifequal i.id interaction.id %}selected {% endifequal %} value="{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=client.name, pk=i.id %}"> {{i.timestamp}}
+ <div class='detail_header'>
+ <h2>{{client.name}}</h2>
+ <a href='{% url reports_client_manage %}#{{ client.name }}'>[manage]</a>
+ <span class='history_links'><a href="{% url reports_client_history client.name %}">View History</a> | Jump to&nbsp;
+ <select id="quick" name="quick" onchange="javascript:pageJump('quick');">
+ <option value="" selected="selected">--- Time ---</option>
+ {% for i in client.interactions.all|slice:":25" %}
+ <option value="{% url reports_client_detail_pk hostname=client.name, pk=i.id %}">{{i.timestamp}}</option>
+ {% endfor %}
+ </select></span>
+ </div>
+
+ {% if interaction.isstale %}
+ <div class="warningbox">
+ This node did not run within the last 24 hours &#8212; it may be out of date.
+ </div>
+ {% endif %}
+ <table class='node_data'>
+ <tr><td>Timestamp</td><td>{{interaction.timestamp}}</td></tr>
+ {% if interaction.server %}
+ <tr><td>Served by</td><td>{{interaction.server}}</td></tr>
+ {% endif %}
+ {% if interaction.repo_rev_code %}
+ <tr><td>Revision</td><td>{{interaction.repo_rev_code}}</td></tr>
+ {% endif %}
+ <tr><td>State</td><td class='{{interaction.state}}-lineitem'>{{interaction.state|capfirst}}</td></tr>
+ <tr><td>Managed entries</td><td>{{interaction.totalcount}}</td></tr>
+ {% if not interaction.isclean %}
+ <tr><td>Deviation</td><td>{{interaction.percentbad|floatformat:"3"}}%</td></tr>
+ {% endif %}
+ </table>
+
+ {% if interaction.bad_entry_count %}
+ <div class='entry_list'>
+ <div class='entry_list_head dirty-lineitem'>
+ <div class='entry_expand_tab' onclick='javascript:toggleMe("bad_table");'>[+]</div>
+ <h3>Bad Entries &#8212; {{ interaction.bad_entry_count }}</h3>
+ </div>
+ <table id='bad_table' class='entry_list'>
+ {% for e in interaction.bad|sortwell %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='entry_list_type'>{{e.entry.kind}}:</td>
+ <td><a href="{% url reports_item "bad",e.id %}">
+ {{e.entry.name}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+
+ {% if interaction.modified_entry_count %}
+ <div class='entry_list'>
+ <div class='entry_list_head modified-lineitem'>
+ <div class='entry_expand_tab' onclick='javascript:toggleMe("modified_table");'>[+]</div>
+ <h3>Modified Entries &#8212; {{ interaction.modified_entry_count }}</h3>
+ </div>
+ <table id='modified_table' class='entry_list'>
+ {% for e in interaction.modified|sortwell %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='entry_list_type'>{{e.entry.kind}}:</td>
+ <td><a href="{% url reports_item "modified",e.id %}">
+ {{e.entry.name}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+
+ {% if interaction.extra_entry_count %}
+ <div class='entry_list'>
+ <div class='entry_list_head extra-lineitem'>
+ <div class='entry_expand_tab' onclick='javascript:toggleMe("extra_table");'>[+]</div>
+ <h3>Extra Entries &#8212; {{ interaction.extra_entry_count }}</h3>
+ </div>
+ <table id='extra_table' class='entry_list'>
+ {% for e in interaction.extra|sortwell %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='entry_list_type'>{{e.entry.kind}}:</td>
+ <td><a href="{% url reports_item "extra",e.id %}">{{e.entry.name}}</a></td>
+ </tr>
{% endfor %}
-</select>
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-<a href="{% url Bcfg2.Server.Reports.reports.views.client_manage hostname=client.name %}">Manage</a> {{client.name}} options.<br/>
+ </table>
+ </div>
+ {% endif %}
-{% include "clients/client-nodebox.html" %}
+ {% if entry_list %}
+ <div class="entry_list recent_history_wrapper">
+ <div class="entry_list_head" style="border-bottom: 2px solid #98DBCC;">
+ <h4 style="display: inline"><a href="{% url reports_client_history client.name %}">Recent Interactions</a></h4>
+ </div>
+ <div class='recent_history_box'>
+ {% include "widgets/interaction_list.inc" %}
+ <div style='padding-left: 5px'><a href="{% url reports_client_history client.name %}">more...</a></div>
+ </div>
+ </div>
+ {% endif %}
{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/clients/detailed-list.html b/src/lib/Server/Reports/reports/templates/clients/detailed-list.html
index 5a1352cff..0c1fae8d5 100644
--- a/src/lib/Server/Reports/reports/templates/clients/detailed-list.html
+++ b/src/lib/Server/Reports/reports/templates/clients/detailed-list.html
@@ -1,57 +1,15 @@
-{% extends "base.html" %}
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
-{% block title %}Detailed Client Listing{% endblock %}
-
-{% block extra_header_info %}
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script type="text/javascript">var cal = new CalendarPopup();</script>
-<style type="text/css">
-#client_list_header {
- font-weight: bold;
- border-bottom:1px solid;
- /*color: #333366;*/
-}
-/*#client_list_box {
- min-width:875px;
-}*/
-.listview {
- padding-top:3px;
- padding-bottom:3px;
-}
-.listview_alt {
- background:#f1ffc9;
- padding-top:3px;
- padding-bottom:3px;
-}
-</style>
-{% endblock%}
-
-{% block pagebanner %}
- <div class="header">
- <h1>Detailed Client List</h1>
- </div>
- <br/>
-{% endblock %}
+{% block title %}Bcfg2 - Detailed Client Listing{% endblock %}
+{% block pagebanner %}Clients - Detailed View{% endblock %}
{% block content %}
-<div>
-<form name="timestamp-select" action='{{ path }}' method='get'>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@
-<input type="text" name="time" value="{{timestamp_time}}" size="8" />
-<a href="#" onclick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" id="anchor1">Calendar</a>
-<input type="button" value="Go" onclick="document.forms['timestamp-select'].submit();"/>
- | <input type="button" name="now" value="Now" onclick="location.href='{{ path }}';"/>
-</span><br/><br/>
-</form>
-</div>
-
-<div id='client_list_box'>
+<div class='client_list_box'>
{% if entry_list %}
+ {% filter_navigator %}
<table cellpadding="3">
- <tr id='client_list_header' class='listview'>
+ <tr id='table_list_header' class='listview'>
<td class='left_column'>Node</td>
<td class='right_column' style='width:75px'>State</td>
<td class='right_column_narrow'>Good</td>
@@ -61,30 +19,19 @@
<td class='right_column'>Last Run</td>
<td class='right_column_wide'>Server</td>
</tr>
- {% for client,entry,stale in entry_list %}
+ {% for entry in entry_list %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='left_column'><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=client, pk=entry.id %}'>{{ client }}</a></td>
- <td class='right_column' style='width:75px'><a href=
- {% if server %}
- '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=server,state=entry.state %}{{ qsa }}'
- {% else %}
- '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list state=entry.state %}{{ qsa }}'
- {% endif %}
- {% ifequal entry.state 'dirty' %}style='background:#FF6A6A'{% endifequal %}>{{ entry.state }}</a></td>
+ <td class='left_column'><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=entry.client.name, pk=entry.id %}'>{{ entry.client.name }}</a></td>
+ <td class='right_column' style='width:75px'><a href='{% add_url_filter state=entry.state %}'
+ {% ifequal entry.state 'dirty' %}class='dirty-lineitem'{% endifequal %}>{{ entry.state }}</a></td>
<td class='right_column_narrow'>{{ entry.goodcount }}</td>
<td class='right_column_narrow'>{{ entry.bad_entry_count }}</td>
<td class='right_column_narrow'>{{ entry.modified_entry_count }}</td>
<td class='right_column_narrow'>{{ entry.extra_entry_count }}</td>
- <td class='right_column'><span {% if stale %}style='background:#FF6A6A'{% endif %}>{{ entry.timestamp|date:"Y-m-d H:i" }}</span></td>
+ <td class='right_column'><span {% if entry.timestamp|isstale:entry_max %}class='dirty-lineitem'{% endif %}>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</span></td>
<td class='right_column_wide'>
{% if entry.server %}
- <a href=
- {% if state %}
- '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=entry.server,state=state %}{{ qsa }}'
- {% else %}
- '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=entry.server %}{{ qsa }}'
- {% endif %}
- >{{ entry.server }}</a>
+ <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a>
{% else %}
&nbsp;
{% endif %}
diff --git a/src/lib/Server/Reports/reports/templates/clients/history.html b/src/lib/Server/Reports/reports/templates/clients/history.html
new file mode 100644
index 000000000..01d4ec2f4
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/clients/history.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Interaction History{% endblock %}
+{% block pagebanner %}Interaction history{% if client %} for {{ client.name }}{% endif %}{% endblock %}
+
+{% block extra_header_info %}
+{% endblock %}
+
+{% block content %}
+<div class='client_list_box'>
+{% if entry_list %}
+ {% filter_navigator %}
+ {% include "widgets/interaction_list.inc" %}
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
+</div>
+{% page_navigator %}
+{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/clients/index.html b/src/lib/Server/Reports/reports/templates/clients/index.html
index cfb8a6c83..e0c0d2d7a 100644
--- a/src/lib/Server/Reports/reports/templates/clients/index.html
+++ b/src/lib/Server/Reports/reports/templates/clients/index.html
@@ -1,56 +1,33 @@
-{% extends "base.html" %}
+{% extends "base-timeview.html" %}
{% block extra_header_info %}
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/sorttable.js"></script>
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script>
{% endblock%}
-{% block title %}Client Index Listing{% endblock %}
+{% block title %}Bcfg2 - Client Grid View{% endblock %}
-{% block pagebanner %}
- <div class="header">
- <h1>Clients List</h1>
- </div>
- <br/>
-{% endblock %}
+{% block pagebanner %}Clients - Grid View{% endblock %}
{% block content %}
-<div>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-</span>
-<form name="timestamp-select" action="{{path}}" method="get">
-<span class="mini-date">
-<input type="text" name="date1" value="{{timestamp_date}}" size=10 />@
-<input type="text" name="time" value="{{timestamp_time}}" size=8 />
-<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" ID="anchor1">Calendar</A>
-<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.client_index %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" />
- | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.client_index %}';"/>
-</span></form>
-<br/><br/><br/></div>
{% if inter_list %}
-<table><tr><td valign="top">
- <ul style="list-style-type:none;">
- {% for client,inter in inter_list %}
- <li><div class="{{inter.state}}-lineitem">
- <a href="{% spaceless %}{% ifequal timestamp 'now' %}
- {% url Bcfg2.Server.Reports.reports.views.client_detail client %}
+ <table class='grid-view' align='center'>
+ {% for inter in inter_list %}
+ {% if forloop.first %}<tr>{% endif %}
+ <td class="{{inter.state}}-lineitem">
+ <a href="{% spaceless %}{% if not timestamp %}
+ {% url reports_client_detail inter.client.name %}
+ {% else %}
+ {% url reports_client_detail_pk inter.client.name,inter.id %}
+ {% endif %}
+ {% endspaceless %}">{{ inter.client.name }}</a>
+ </td>
+ {% if forloop.last %}
+ </tr>
{% else %}
- {% url Bcfg2.Server.Reports.reports.views.client_detail client,inter.id %}
- {% endifequal %}
- {% endspaceless %}">{{ client }}</a>
- </div></li>
- {% ifequal half_list forloop.counter0 %}
- </ul>
-</td><td valign="top">
- <ul style="list-style-type:none;">
- {% endifequal %}
+ {% if forloop.counter|divisibleby:"4" %}</tr><tr>{% endif %}
+ {% endif %}
{% endfor %}
- </ul>
-</tr></table>
+ </table>
{% else %}
<p>No client records are available.</p>
{% endif %}
diff --git a/src/lib/Server/Reports/reports/templates/clients/manage.html b/src/lib/Server/Reports/reports/templates/clients/manage.html
index 61f0fe017..5725ae577 100644
--- a/src/lib/Server/Reports/reports/templates/clients/manage.html
+++ b/src/lib/Server/Reports/reports/templates/clients/manage.html
@@ -1,29 +1,45 @@
{% extends "base.html" %}
+
{% block extra_header_info %}
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script>
{% endblock%}
-{% block title %}{{client.name}}{% endblock %}
-{% block content %}
-<h2>Client Options Management page for {{client.name}}</h2><br/>
-<p>Client status detail page: <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail client.name %}">{{client.name}}</a>.</p>
-<p>Hosts may be prevented from showing up in the reporting system if they have been retired, are no longer managed by bcfg2 :(, etc. </p>
-<b>Select deactivation date: </b>
-<div>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-</span>
-<form name="timestamp-select" action="{% url Bcfg2.Server.Reports.reports.views.client_manage client.name %}" method="post">
-<span class="mini-date">
-<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@
-<input type="text" name="time" value="{{timestamp_time}}" size="8" />
-<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" ID="anchor1">Calendar</a>
-<input type="submit" value="Submit">
-</span></form>
-<br/><br/><br/></div>
-<br/><br/>
-<p>{{message}}</p>
+{% block title %}Bcfg2 - Manage Clients{% endblock %}
+{% block pagebanner %}Clients - Manage{% endblock %}
+
+{% block content %}
+<div class='client_list_box'>
+ {% if message %}
+ <div class="warningbox">{{ message }}</div>
+ {% endif %}
+{% if clients %}
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td class='left_column'>Node</td>
+ <td class='right_column'>Expiration</td>
+ <td class='right_column_narrow'>Manage</td>
+ </tr>
+ {% for client in clients %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><span id="{{ client.name }}"> </span>
+ <span id="ttag-{{ client.name }}"> </span>
+ <span id="s-ttag-{{ client.name }}"> </span>
+ <a href="{% url reports_client_detail client.name %}">{{ client.name }}</a></td>
+ <td>{% firstof client.expiration 'Active' %}</td>
+ <td>
+ <form method="post" action="{% url reports_client_manage %}">
+ <div> {# here for no reason other then to validate #}
+ <input type="hidden" name="client_name" value="{{ client.name }}" />
+ <input type="hidden" name="client_action" value="{% if client.expiration %}unexpire{% else %}expire{% endif %}" />
+ <input type="submit" value="{% if client.expiration %}Activate{% else %}Expire Now{% endif %}" />
+ </div>
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/config_items/index.html b/src/lib/Server/Reports/reports/templates/config_items/index.html
deleted file mode 100644
index 04083344c..000000000
--- a/src/lib/Server/Reports/reports/templates/config_items/index.html
+++ /dev/null
@@ -1,100 +0,0 @@
-{% extends "base.html" %}
-
-{% load syntax_coloring %}
-
-{% block extra_header_info %}
-<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/syntax-coloring.css" />
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script>
-{% endblock%}
-{% block title %}Configuration Element Details{% endblock %}
-
-{% block pagebanner %}
- <div class="header">
- <h1>Configuration Element Details</h1>
- </div>
- <br/>
-{% endblock %}
-
-{% block content %}
-
-{% ifequal mod_or_bad "bad" %}
-<div class="bad">
-<h2>Bad {{item.entry.kind}}: {{item.entry.name}}</h2>
-</div>
-{% else %}
-<div class="modified">
-<h2>Modified {{item.entry.kind}}: {{item.entry.name}}</h2>
-</div>
-{% endifequal %}
-<center>
-<table border=1 padding=0 >
-<tr><th>Reason</th><th>Current Status</th><th>Specified in bcfg2</th></tr>
-{% if item.reason.current_owner %}
-<tr><td align="right"><b>Owner: </b></td><td>{{item.reason.current_owner}}</td><td>{{item.reason.owner}}</td></tr>
-{% endif %}{% if item.reason.current_group %}
-<tr><td align="right"><b>Group: </b></td><td>{{item.reason.current_group}}</td><td>{{item.reason.group}}</td></tr>
-{% endif %}{% if item.reason.current_perms %}
-<tr><td align="right"><b>Permissions: </b></td><td>{{item.reason.current_perms}}</td><td>{{item.reason.perms}}</td></tr>
-{% endif %}{% if item.reason.current_status %}
-<tr><td align="right"><b>Status: </b></td><td>{{item.reason.current_status}}</td><td>{{item.reason.status}}</td></tr>
-{% endif %}{% if item.reason.current_to %}
-<tr><td align="right"><b>Link Destination: </b></td><td>{{item.reason.current_to}}</td><td>{{item.reason.to}}</td></tr>
-{% endif %}{% if item.reason.current_version %}
-<tr><td align="right"><b>Version: </b></td><td>{{item.reason.current_version}}</td><td>{{item.reason.version}}</td></tr>
-{% endif %}{% if not item.reason.current_exists %}
-<tr><td align="right"><b>Existence: </b></td><td colspan=2>This item does not currently exist on the host but is specified to exist in the configuration.</td></tr>
-{% endif %}{% if item.reason.current_diff %}
-<tr><td align="right"><b>NDiff: </b></td><td colspan=2><pre>{{item.reason.current_diff|syntaxhilight:"diff"}}</pre></td></tr>
-{% endif %}
-</table></center>
-<hr/>
-<div>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-</span>
-<form name="timestamp-select" action="{{path}}" method="get">
-<span class="mini-date">
-<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@
-<input type="text" name="time" value="{{timestamp_time}}" size="8" />
-<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" ID="anchor1">Calendar</A>
-{% ifequal mod_or_bad "modified" %}
- <input type="button"
- name="go"
- value="Go"
- onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id%}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" />
- | <input type="button"
- name="now"
- value="Now"
- onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id %}';"/>
-{% else %}
- <input type="button"
- name="go"
- value="Go"
- onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;"/>
- | <input type="button"
- name="now"
- value="Now"
- onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id %}';"/>
-{% endifequal %}
-</span></form>
-<br/><br/><br/></div>
-{% if associated_client_list %}
- <p>The following clients had this problem as of {{timestamp_date}}@{{timestamp_time}}:</p>
- {% for client in associated_client_list %}
- <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail client.name %}">{{client.name}}</a><br/>
- {% endfor %}
- <br />
- <br />
-{% else %}
- <p>No Clients had this problem at {{timestamp}}</p>
-{% endif %}
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/config_items/item.html b/src/lib/Server/Reports/reports/templates/config_items/item.html
new file mode 100644
index 000000000..41474922b
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/config_items/item.html
@@ -0,0 +1,109 @@
+{% extends "base.html" %}
+{% load syntax_coloring %}
+
+
+{% block title %}Bcfg2 - Element Details{% endblock %}
+
+
+{% block extra_header_info %}
+<style type="text/css">
+#table_list_header {
+ font-size: 100%;
+}
+table.entry_list {
+ width: auto;
+}
+div.information_wrapper {
+ margin: 15px;
+}
+div.diff_wrapper {
+ overflow: auto;
+}
+div.entry_list h3 {
+ font-size: 90%;
+ padding: 5px;
+}
+</style>
+{% endblock%}
+
+{% block pagebanner %}Element Details{% endblock %}
+
+{% block content %}
+ <div class='detail_header'>
+ <h3>{{mod_or_bad|capfirst}} {{item.entry.kind}}: {{item.entry.name}}</h3>
+ </div>
+
+ <div class="information_wrapper">
+
+ {% if isextra %}
+ <p>This item exists on the host but is not defined in the configuration.</p>
+ {% endif %}
+
+ {% if not item.reason.current_exists %}
+ <div class="warning">This item does not currently exist on the host but is specified to exist in the configuration.</div>
+ {% endif %}
+
+ {% if item.reason.current_owner or item.reason.current_group or item.reason.current_perms or item.reason.current_status or item.reason.current_status or item.reason.current_to or item.reason.current_version %}
+ <table class='entry_list'>
+ <tr id='table_list_header'>
+ <td style='text-align: right;'>Problem Type</td><td>Expected</td><td style='border-bottom: 1px solid #98DBCC;'>Found</td></tr>
+ {% if item.reason.current_owner %}
+ <tr><td style='text-align: right'><b>Owner</b></td><td>{{item.reason.owner}}</td>
+ <td>{{item.reason.current_owner}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_group %}
+ <tr><td style='text-align: right'><b>Group</b></td><td>{{item.reason.group}}</td>
+ <td>{{item.reason.current_group}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_perms %}
+ <tr><td style='text-align: right'><b>Permissions</b></td><td>{{item.reason.perms}}</td>
+ <td>{{item.reason.current_perms}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_status %}
+ <tr><td style='text-align: right'><b>Status</b></td><td>{{item.reason.status}}</td>
+ <td>{{item.reason.current_status}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_to %}
+ <tr><td style='text-align: right'><b>Symlink Target</b></td><td>{{item.reason.to}}</td>
+ <td>{{item.reason.current_to}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_version %}
+ <tr><td style='text-align: right'><b>Package Version</b></td><td>{{item.reason.version|cut:"("|cut:")"}}</td>
+ <td>{{item.reason.current_version|cut:"("|cut:")"}}</td></tr>
+ {% endif %}
+ </table>
+ {% endif %}
+
+ {% if item.reason.current_diff %}
+ <div class='entry_list'>
+ <div class='entry_list_head'>
+ <h3>Incorrect file contents</h3>
+ </div>
+ <div class='diff_wrapper'>
+ {{ item.reason.current_diff|syntaxhilight }}
+ </div>
+ </div>
+ {% endif %}
+
+
+ <div class='entry_list'>
+ <div class='entry_list_head'>
+ <h3>Occurances on {{ timestamp|date:"Y-m-d" }}</h3>
+ </div>
+ {% if associated_list %}
+ <table class="entry_list" cellpadding="3">
+ {% for inter in associated_list %}
+ <tr><td><a href="{% url reports_client_detail inter.client.name %}"
+ >{{inter.client.name}}</a></td>
+ <td><a href="{% url reports_client_detail_pk hostname=inter.client.name,pk=inter.id %}"
+ >{{inter.timestamp}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>Missing client list</p>
+ {% endif %}
+ </div>
+
+ </div><!-- information_wrapper -->
+{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/config_items/listing.html b/src/lib/Server/Reports/reports/templates/config_items/listing.html
index 64a60e506..572249470 100644
--- a/src/lib/Server/Reports/reports/templates/config_items/listing.html
+++ b/src/lib/Server/Reports/reports/templates/config_items/listing.html
@@ -1,50 +1,32 @@
-{% extends "base.html" %}
-{% load django_templating_sigh %}
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
-{% block extra_header_info %}
-<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/tabview/assets/tabview.css" />
-<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/round_tabs.css" />
-
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/yahoo/yahoo.js"></script>
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/event/event.js"></script>
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/dom/dom.js"></script>
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/tabview/tabview.js"></script>
-<script type="text/javascript">
-YAHOO.example.init = function( ){
- var tabView = new YAHOO.widget.TabView( { id: 'demo' } );
- {% for item_list in item_list_pseudodict %}
- tabView.addTab( new YAHOO.widget.Tab({
- label: '{{item_list.0}}',
- content: '<p><ul style="list-style-type:none;">{% for item in item_list.1|sortwell %}<li><strong>{{item.entry.kind}}: <'+'/strong><tt>{% ifequal mod_or_bad "modified" %}<a href="{%url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id%}">{{item.entry.name}}<'+'/a>{% else %}<a href="{%url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id%}">{{item.entry.name}}<'+'/a>{% endifequal %}<'+'/tt><'+'/li>{% endfor %}<'+'/ul><'+'/p>',
- active: 'True'
- }));
- {% endfor %}
+{% block title %}Bcfg2 - Element Listing{% endblock %}
- YAHOO.util.Event.onContentReady('tabview', function() {
- tabView.appendTo(this); /* append to #doc */
- });
-
-};
-YAHOO.example.init();
+{% block extra_header_info %}
+{% endblock%}
-</script>
-<style type="text/css">
-#demo .yui-content { padding:1em; } /* pad content container */
-</style>
-{% endblock %}
+{% block pagebanner %}{{mod_or_bad|capfirst}} Element Listing{% endblock %}
-{% block title %}{{mod_or_bad|capfirst}} Item Listing{% endblock %}
+{% block content %}
+{% if item_list_dict %}
+ {% for kind, entries in item_list_dict.items %}
-{% block pagebanner %}
- <div class="header">
- <h1>{{mod_or_bad|capfirst}} Configuration Elements</h1>
+ <div class='entry_list'>
+ <div class='entry_list_head element_list_head'>
+ <div class='entry_expand_tab' onclick='javascript:toggleMe("table_{{ kind }}");'>[+]</div>
+ <h3>{{ kind }} &#8212; {{ entries|length }}</h3>
</div>
- <br/>
-{% endblock %}
-{% block content %}
-{% if item_list_pseudodict %}
-<div id="tabview"></div>
+ <table id='table_{{ kind }}' class='entry_list'>
+ {% for e in entries %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href="{% url reports_item type=mod_or_bad,pk=e.id %}">{{e.entry.name}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
{% else %}
<p>There are currently no inconsistent configuration entries.</p>
{% endif %}
diff --git a/src/lib/Server/Reports/reports/templates/displays/index.html b/src/lib/Server/Reports/reports/templates/displays/index.html
deleted file mode 100644
index c078539b6..000000000
--- a/src/lib/Server/Reports/reports/templates/displays/index.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Display Index Listing{% endblock %}
-{% block pagebanner %}
- <div class="header">
- <h1>BCFG Display Index</h1>
- {% comment %} <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>{% endcomment %}
- </div>
- <br/>
-{% endblock %}
-
-{% block content %}
-<ul>
-<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_sys_view %}">System View</a></li>
-<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_summary %}">Summary Only</a></li>
-<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_timing %}">Timing</a></li>
-</ul>
-{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html b/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html
deleted file mode 100644
index 60f97eadc..000000000
--- a/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "displays/summary-block.html" %}
-{% block linkprefix1 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
-{% block linkprefix2 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
-{% block linkprefix3 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
-{% block linkprefix4 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
-{% block linkprefix5 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
-{% block linkprefix6 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} \ No newline at end of file
diff --git a/src/lib/Server/Reports/reports/templates/displays/summary-block.html b/src/lib/Server/Reports/reports/templates/displays/summary-block.html
deleted file mode 100644
index 060ff0fa1..000000000
--- a/src/lib/Server/Reports/reports/templates/displays/summary-block.html
+++ /dev/null
@@ -1,90 +0,0 @@
-{% load django_templating_sigh %}
-
- <div class="nodebox">
- <h2>Summary:</h2>
- <p class="indented">{{client_list|length }} Nodes were included in your report.</p>
- {% if clean_client_list %}
- <div class="clean">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('goodsummary');" title="Click to Expand" class="commentLink">{{clean_client_list|length}}</a> nodes are clean.<br /></span>
- <div class="items" id="goodsummary"><ul class="plain">
- {% for client in clean_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix1 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if bad_client_list %}
- <div class="bad">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('badsummary');" title="Click to Expand" class="commentLink">{{bad_client_list|length}}</a> nodes are bad.<br /></span>
- <div class="items" id="badsummary"><ul class="plain">
- {% for client in bad_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix2 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if modified_client_list %}
- <div class="modified">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('modifiedsummary');" title="Click to Expand" class="commentLink">{{modified_client_list|length}}</a> nodes were modified in the previous run.<br /></span>
- <div class="items" id="modifiedsummary"><ul class="plain">
- {% for client in modified_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix3 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if extra_client_list %}
- <div class="extra">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('extrasummary');" title="Click to Expand" class="commentLink">{{extra_client_list|length}}</a> nodes have extra configuration. (includes both good and bad nodes)<br /></span>
- <div class="items" id="extrasummary"><ul class="plain">
- {% for client in extra_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix4 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if stale_up_client_list %}
- <div class="warning">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('vstalesummary');" title="Click to Expand" class="commentLink">{{stale_up_client_list|length}}</a> nodes did not run within the last 24 hours but were pingable.<br /></span>
- <div class="items" id="vstalesummary"><ul class="plain">
- {% for client in stale_up_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix5 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if stale_all_client_list %}
- <div class="all-warning">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('stalesummary');" title="Click to Expand" class="commentLink">{{stale_all_client_list|length}}</a> nodes did not run within the last 24 hours. (includes nodes up and down)<br /></span>
- <div class="items" id="stalesummary"><ul class="plain">
- {% for client in stale_all_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="{% block linkprefix6 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- {% if down_client_list %}
- <div class="down">
- <span class="nodelisttitle"><a href="javascript:toggleLayer('unpingablesummary');" title="Click to Expand" class="commentLink">{{down_client_list|length}}</a> nodes were down.<br /></span>
- <div class="items" id="unpingablesummary"><ul class="plain">
- {% for client in down_client_list|sortname %}
- {% set_interaction "foo" %}
- <li><b>Node: </b>
- <tt><a href="#{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li>
- {% endfor %}
- </ul></div>
- </div>
- {% endif %}
- </div>
diff --git a/src/lib/Server/Reports/reports/templates/displays/summary.html b/src/lib/Server/Reports/reports/templates/displays/summary.html
index 29cbb22d7..0124f635d 100644
--- a/src/lib/Server/Reports/reports/templates/displays/summary.html
+++ b/src/lib/Server/Reports/reports/templates/displays/summary.html
@@ -1,31 +1,42 @@
-{% extends "base.html" %}
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Client Summary{% endblock %}
+{% block pagebanner %}Clients - Summary{% endblock %}
+
+{% block body_onload %}javascript:hide_table_array(hide_tables){% endblock %}
+
{% block extra_header_info %}
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script>
+<script type="text/javascript">
+var hide_tables = new Array({{ summary_data|length }});
+{% for summary in summary_data %}
+hide_tables[{{ forloop.counter0 }}] = "table_{{ summary.name }}";
+{% endfor %}
+</script>
{% endblock%}
-{% block title %}Display Index Listing{% endblock %}
-{% block pagebanner %}
- <div class="header">
- <h1>BCFG Clients Summary</h1>
- <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>
- </div>
- <br/>
-{% endblock %}
{% block content %}
-<div>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-</span>
-<form name="timestamp-select" action="{{path}" method="get">
-<span class="mini-date">
-<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@
-<input type="text" name="time" value="{{timestamp_time}}" size="8" />
-<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" ID="anchor1">Calendar</A>
-<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_summary %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" />
- | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_summary %}';"/>
-</span></form>
-<br/><br/><br/></div>
- {% include "displays/summary-block-direct-links.html" %}
+ <div class='detail_header'>
+ <h2>{{ node_count }} nodes reporting in</h2>
+ </div>
+{% if summary_data %}
+ {% for summary in summary_data %}
+ <div class='entry_list'>
+ <div class='entry_list_head element_list_head'>
+ <div class='entry_expand_tab' onclick='javascript:toggleMe("table_{{ summary.name }}");'>[+]</div>
+ <h3>{{ summary.nodes|length }} {{ summary.label }}</h3>
+ </div>
+
+ <table id='table_{{ summary.name }}' class='entry_list'>
+ {% for node in summary.nodes|sort_interactions_by_name %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href="{% url reports_client_detail_pk hostname=node.client.name,pk=node.id %}">{{ node.client.name }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
+{% else %}
+ <p>No data to report on</p>
+{% endif %}
{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/displays/sys_view.html b/src/lib/Server/Reports/reports/templates/displays/sys_view.html
deleted file mode 100644
index 1298059bf..000000000
--- a/src/lib/Server/Reports/reports/templates/displays/sys_view.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% load django_templating_sigh %}
-
-{% block title %}System-View Display{% endblock %}
-{% block pagebanner %}
- <div class="header">
- <h1>Grand System View</h1>
- <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>
- </div>
- <br/>
-{% endblock %}
-{% block content %}
-<center><h2>This view is deprecated and will be removed soon.</h2><br/>Please use the "Summary" view and drill down instead.</center>
-
- {% include "displays/summary-block.html" %}
- {% for client in client_list %}
- {% set_interaction "foo" %}
- {% include "clients/client-nodebox.html" %}
- {% endfor %}
-{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/displays/timing.html b/src/lib/Server/Reports/reports/templates/displays/timing.html
index 32ddab464..47accb2cb 100644
--- a/src/lib/Server/Reports/reports/templates/displays/timing.html
+++ b/src/lib/Server/Reports/reports/templates/displays/timing.html
@@ -1,54 +1,38 @@
-{% extends "base.html" %}
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Performance Metrics{% endblock %}
+{% block pagebanner %}Performance Metrics{% endblock %}
+
{% block extra_header_info %}
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/sorttable.js"></script>
-<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script>
-<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script>
{% endblock%}
-{% block title %}Display Index Listing{% endblock %}
{% block content %}
- <div class="header">
- <h1>BCFG Performance Timings</h1>
- <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>
- </div>
- <br/>
-<div>
-<span class="mini-date">
-<b>Enter date or use calendar popup: </b>
-</span>
-<form name="timestamp-select" action="{{path}}" method="get">
-<span class="mini-date">
-<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@
-<input type="text" name="time" value="{{timestamp_time}}" size="8" />
-<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;"
- name="anchor1" ID="anchor1">Calendar</A>
-<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_timing %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" />
- | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_timing %}';"/>
-</span></form>
-<br/><br/><br/></div>
- <center>
- <table id="t1" class="sortable">
- <tr>
- <th class="sortable">Hostname</th>
- <th class="sortable">Parse</th>
- <th class="sortable">Probe</th>
- <th class="sortable">Inventory</th>
- <th class="sortable">Install</th>
- <th class="sortable">Config</th>
- <th class="sortable">Total</th>
+<div class='client_list_box'>
+ {% if metrics %}
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td>Name</td>
+ <td>Parse</td>
+ <td>Probe</td>
+ <td>Inventory</td>
+ <td>Install</td>
+ <td>Config</td>
+ <td>Total</td>
</tr>
- {% for dict_unit in stats_list %}
- <tr>
- <td class="sortable"><a href="{% url Bcfg2.Server.Reports.reports.views.client_detail dict_unit.name%}/">{{dict_unit.name}}</a></td>
- <td class="sortable">{{dict_unit.parse}}</td>
- <td class="sortable">{{dict_unit.probe}}</td>
- <td class="sortable">{{dict_unit.inventory}}</td>
- <td class="sortable">{{dict_unit.install}}</td>
- <td class="sortable">{{dict_unit.config}}</td>
- <td class="sortable">{{dict_unit.total}}</td>
+ {% for metric in metrics|dictsort:"name" %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a style='font-size: 100%'
+ href="{% url reports_client_detail hostname=metric.name %}">{{ metric.name }}</a></td>
+ {% for mitem in metric|build_metric_list %}
+ <td>{{ mitem }}</td>
+ {% endfor %}
</tr>
{% endfor %}
</table>
- </center>
+ {% else %}
+ <p>No metric data available</p>
+ {% endif %}
+</div>
{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/index.html b/src/lib/Server/Reports/reports/templates/index.html
deleted file mode 100644
index 002a3f770..000000000
--- a/src/lib/Server/Reports/reports/templates/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "base.html" %}
-
-{% block pagebanner %}
- <div class="header">
- <h1>BCFG Reports</h1>
- {% comment %} <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>{% endcomment %}
- </div>
- <br/>
-{% endblock %}
-{% block content %}
-<h1>Welcome to the Bcfg2 Reporting System</h1>
-<p>
-Please use the links at the left to navigate.
-</p>
-{% endblock %}
diff --git a/src/lib/Server/Reports/reports/templates/widgets/filter_bar.html b/src/lib/Server/Reports/reports/templates/widgets/filter_bar.html
new file mode 100644
index 000000000..6b57baf6a
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/widgets/filter_bar.html
@@ -0,0 +1,13 @@
+{% spaceless %}
+{% if filters %}
+{% for filter, filter_url in filters %}
+ {% if forloop.first %}
+ <div class="filter_bar">Active filters (click to remove):
+ {% endif %}
+ <a href='{{ filter_url }}'>{{ filter|capfirst }}</a>{% if not forloop.last %}, {% endif %}
+ {% if forloop.last %}
+ </div>
+ {% endif %}
+{% endfor %}
+{% endif %}
+{% endspaceless %}
diff --git a/src/lib/Server/Reports/reports/templates/widgets/interaction_list.inc b/src/lib/Server/Reports/reports/templates/widgets/interaction_list.inc
new file mode 100644
index 000000000..8f2dec1dc
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/widgets/interaction_list.inc
@@ -0,0 +1,38 @@
+{% load bcfg2_tags %}
+<div class='interaction_history_widget'>
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td class='left_column'>Timestamp</td>
+ {% if not client %}
+ <td class='right_column_wide'>Client</td>
+ {% endif %}
+ <td class='right_column' style='width:75px'>State</td>
+ <td class='right_column_narrow'>Good</td>
+ <td class='right_column_narrow'>Bad</td>
+ <td class='right_column_narrow'>Modified</td>
+ <td class='right_column_narrow'>Extra</td>
+ <td class='right_column_wide'>Server</td>
+ </tr>
+ {% for entry in entry_list %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='left_column'><a href='{% url reports_client_detail_pk hostname=entry.client.name, pk=entry.id %}'>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</a></td>
+ {% if not client %}
+ <td class='right_column_wide'><a href='{% add_url_filter hostname=entry.client.name %}'>{{ entry.client.name }}</a></td>
+ {% endif %}
+ <td class='right_column' style='width:75px'><a href='{% add_url_filter state=entry.state %}'
+ {% ifequal entry.state 'dirty' %}class='dirty-lineitem'{% endifequal %}>{{ entry.state }}</a></td>
+ <td class='right_column_narrow'>{{ entry.goodcount }}</td>
+ <td class='right_column_narrow'>{{ entry.bad_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.modified_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.extra_entry_count }}</td>
+ <td class='right_column_wide'>
+ {% if entry.server %}
+ <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a>
+ {% else %}
+ &nbsp;
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+</div>
diff --git a/src/lib/Server/Reports/reports/templates/widgets/page_bar.html b/src/lib/Server/Reports/reports/templates/widgets/page_bar.html
new file mode 100644
index 000000000..aa0def83e
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templates/widgets/page_bar.html
@@ -0,0 +1,23 @@
+{% spaceless %}
+{% for page, page_url in pager %}
+ {% if forloop.first %}
+ <div class="page_bar">
+ {% if prev_page %}<a href="{{ prev_page }}">&lt; Prev</a><span>&nbsp;</span>{% endif %}
+ {% if first_page %}<a href="{{ first_page }}">1</a><span>&nbsp;...&nbsp;</span>{% endif %}
+ {% endif %}
+ {% ifequal page current_page %}
+ <span class='nav_bar_current'>{{ page }}</span>
+ {% else %}
+ <a href="{{ page_url }}">{{ page }}</a>
+ {% endifequal %}
+ {% if forloop.last %}
+ {% if last_page %}<span>&nbsp;...&nbsp;</span><a href="{{ last_page }}">{{ total_pages }}</a><span>&nbsp;</span>{% endif %}
+ {% if next_page %}<a href="{{ next_page }}">Next &gt;</a><span>&nbsp;</span>{% endif %}
+ |{% for limit, limit_url in page_limits %}&nbsp;<a href="{{ limit_url }}">{{ limit }}</a>{% endfor %}
+ </div>
+ {% else %}
+ <span>&nbsp;</span>
+ {% endif %}
+{% endfor %}
+{% endspaceless %}
+<!-- {{ path }} -->
diff --git a/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py b/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py
new file mode 100644
index 000000000..2c27aab04
--- /dev/null
+++ b/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py
@@ -0,0 +1,239 @@
+from django import template
+from django.core.urlresolvers import resolve, reverse, Resolver404, NoReverseMatch
+from django.utils.encoding import smart_unicode, smart_str
+from datetime import datetime, timedelta
+from Bcfg2.Server.Reports.utils import filter_list
+
+register = template.Library()
+
+__PAGE_NAV_LIMITS__ = (10, 25, 50, 100)
+
+@register.inclusion_tag('widgets/page_bar.html', takes_context=True)
+def page_navigator(context):
+ """
+ Creates paginated links.
+
+ Expects the context to be a RequestContext and views.prepare_paginated_list()
+ to have populated page information.
+ """
+ fragment = dict()
+ try:
+ path = context['request'].path
+ total_pages = int(context['total_pages'])
+ records_per_page = int(context['records_per_page'])
+ except KeyError, e:
+ return fragment
+ except ValueError, e:
+ return fragment
+
+ if total_pages < 2:
+ return {}
+
+ try:
+ view, args, kwargs = resolve(path)
+ current_page = int(kwargs.get('page_number',1))
+ fragment['current_page'] = current_page
+ fragment['page_number'] = current_page
+ fragment['total_pages'] = total_pages
+ fragment['records_per_page'] = records_per_page
+ if current_page > 1:
+ kwargs['page_number'] = current_page - 1
+ fragment['prev_page'] = reverse(view, args=args, kwargs=kwargs)
+ if current_page < total_pages:
+ kwargs['page_number'] = current_page + 1
+ fragment['next_page'] = reverse(view, args=args, kwargs=kwargs)
+
+ view_range = 5
+ if total_pages > view_range:
+ pager_start = current_page - 2
+ pager_end = current_page + 2
+ if pager_start < 1:
+ pager_end += (1 - pager_start)
+ pager_start = 1
+ if pager_end > total_pages:
+ pager_start -= (pager_end - total_pages)
+ pager_end = total_pages
+ else:
+ pager_start = 1
+ pager_end = total_pages
+
+ if pager_start > 1:
+ kwargs['page_number'] = 1
+ fragment['first_page'] = reverse(view, args=args, kwargs=kwargs)
+ if pager_end < total_pages:
+ kwargs['page_number'] = total_pages
+ fragment['last_page'] = reverse(view, args=args, kwargs=kwargs)
+
+ pager = []
+ for page in range(pager_start, int(pager_end) + 1):
+ kwargs['page_number'] = page
+ pager.append( (page, reverse(view, args=args, kwargs=kwargs)) )
+
+ kwargs['page_number'] = 1
+ page_limits = []
+ for limit in __PAGE_NAV_LIMITS__:
+ kwargs['page_limit'] = limit
+ page_limits.append( (limit, reverse(view, args=args, kwargs=kwargs)) )
+ # resolver doesn't like this
+ del kwargs['page_number']
+ del kwargs['page_limit']
+ page_limits.append( ('all', reverse(view, args=args, kwargs=kwargs) + "|all") )
+
+ fragment['pager'] = pager
+ fragment['page_limits'] = page_limits
+
+ except Resolver404:
+ path = "404"
+ except NoReverseMatch, nr:
+ path = "NoReverseMatch: %s" % nr
+ except ValueError:
+ path = "ValueError"
+ #FIXME - Handle these
+
+ fragment['path'] = path
+ return fragment
+
+@register.inclusion_tag('widgets/filter_bar.html', takes_context=True)
+def filter_navigator(context):
+ try:
+ path = context['request'].path
+ view, args, kwargs = resolve(path)
+
+ # Strip any page limits and numbers
+ if 'page_number' in kwargs:
+ del kwargs['page_number']
+ if 'page_limit' in kwargs:
+ del kwargs['page_limit']
+
+ filters = []
+ for filter in filter_list:
+ if filter in kwargs:
+ myargs = kwargs.copy()
+ del myargs[filter]
+ filters.append( (filter, reverse(view, args=args, kwargs=myargs) ) )
+ filters.sort(lambda x,y: cmp(x[0], y[0]))
+ return { 'filters': filters }
+ except (Resolver404, NoReverseMatch, ValueError, KeyError):
+ pass
+ return dict()
+
+def _subtract_or_na(mdict, x, y):
+ """
+ Shortcut for build_metric_list
+ """
+ try:
+ return round(mdict[x] - mdict[y], 4)
+ except:
+ return "n/a"
+
+@register.filter
+def build_metric_list(mdict):
+ """
+ Create a list of metric table entries
+
+ Moving this here it simplify the view. Should really handle the case where these
+ are missing...
+ """
+ td_list = []
+ # parse
+ td_list.append( _subtract_or_na(mdict, 'config_parse', 'config_download'))
+ #probe
+ td_list.append( _subtract_or_na(mdict, 'probe_upload', 'start'))
+ #inventory
+ td_list.append( _subtract_or_na(mdict, 'inventory', 'initialization'))
+ #install
+ td_list.append( _subtract_or_na(mdict, 'install', 'inventory'))
+ #cfg download & parse
+ td_list.append( _subtract_or_na(mdict, 'config_parse', 'probe_upload'))
+ #total
+ td_list.append( _subtract_or_na(mdict, 'finished', 'start'))
+ return td_list
+
+@register.filter
+def isstale(timestamp, entry_max=None):
+ """
+ Check for a stale timestamp
+
+ Compares two timestamps and returns True if the
+ difference is greater then 24 hours.
+ """
+ if not entry_max:
+ entry_max = datetime.now()
+ return entry_max - timestamp > timedelta(hours=24)
+
+@register.filter
+def sort_interactions_by_name(value):
+ """
+ Sort an interaction list by client name
+ """
+ inters = list(value)
+ inters.sort(lambda a,b: cmp(a.client.name, b.client.name))
+ return inters
+
+class AddUrlFilter(template.Node):
+ def __init__(self, filter_name, filter_value):
+ self.filter_name = filter_name
+ self.filter_value = filter_value
+ self.fallback_view = 'Bcfg2.Server.Reports.reports.views.render_history_view'
+
+ def render(self, context):
+ link = '#'
+ try:
+ path = context['request'].path
+ view, args, kwargs = resolve(path)
+ filter_value = self.filter_value.resolve(context, True)
+ if filter_value:
+ filter_name = smart_str(self.filter_name)
+ filter_value = smart_unicode(filter_value)
+ kwargs[filter_name] = filter_value
+ # These two don't make sense
+ if filter_name == 'server' and 'hostname' in kwargs:
+ del kwargs['hostname']
+ elif filter_name == 'hostname' and 'server' in kwargs:
+ del kwargs['server']
+ try:
+ link = reverse(view, args=args, kwargs=kwargs)
+ except NoReverseMatch, rm:
+ link = reverse(self.fallback_view, args=None,
+ kwargs={ filter_name: filter_value })
+ except NoReverseMatch, rm:
+ raise rm
+ except (Resolver404, ValueError), e:
+ pass
+ return link
+
+@register.tag
+def add_url_filter(parser, token):
+ """
+ Return a url with the filter added to the current view.
+
+ Takes a new filter and resolves the current view with the new filter
+ applied. Resolves to Bcfg2.Server.Reports.reports.views.client_history
+ by default.
+
+ {% add_url_filter server=interaction.server %}
+ """
+ try:
+ tag_name, filter_pair = token.split_contents()
+ filter_name, filter_value = filter_pair.split('=', 1)
+ filter_name = filter_name.strip()
+ filter_value = parser.compile_filter(filter_value)
+ except ValueError:
+ raise template.TemplateSyntaxError, "%r tag requires exactly one argument" % token.contents.split()[0]
+ if not filter_name or not filter_value:
+ raise template.TemplateSyntaxError, "argument should be a filter=value pair"
+
+ return AddUrlFilter(filter_name, filter_value)
+
+@register.filter
+def sortwell(value):
+ """
+ Sorts a list(or evaluates queryset to list) of bad, extra, or modified items in the best
+ way for presentation
+ """
+
+ configItems = list(value)
+ configItems.sort(lambda x,y: cmp(x.entry.name, y.entry.name))
+ configItems.sort(lambda x,y: cmp(x.entry.kind, y.entry.kind))
+ return configItems
+
diff --git a/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py b/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py
deleted file mode 100644
index c0d05d2c1..000000000
--- a/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from django import template
-#from Bcfg2.Server.Reports.reports.models import Client, Interaction, Bad, Modified, Extra
-
-register = template.Library()
-
-def set_interaction(parser, token):
- try:
- # Splitting by None == splitting by spaces.
- tag_name, format_string = token.contents.split(None, 1)
- except ValueError:
- raise template.TemplateSyntaxError, "%r tag requires an argument" % token.contents[0]
- if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
- raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name
- return SetInteraction(format_string[1:-1])
-
-def sortwell(value):
- "sorts a list(or evaluates queryset to list) of bad, extra, or modified items in the best"
- "way for presentation"
- configItems = list(value)
- configItems.sort(lambda x,y: cmp(x.entry.name, y.entry.name))
- configItems.sort(lambda x,y: cmp(x.entry.kind, y.entry.kind))
- return configItems
-def sortname(value):
- "sorts a list( or evaluates queryset) by name"
- configItems = list(value)
- configItems.sort(lambda x,y: cmp(x.name, y.name))
- return configItems
-
-class SetInteraction(template.Node):
- def __init__(self, times):
- self.times = times#do soemthing to select different interaction with host?
- def render(self, context):
- try:
- context['interaction'] = context['client_interaction_dict'][context['client'].id]
- except:#I don't fully know what the implications of this are.
- pass
- return ''
-
-register.tag('set_interaction', set_interaction)
-register.filter('sortwell', sortwell)
-register.filter('sortname', sortname)
diff --git a/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py b/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py
index 083b83a73..43dafb262 100644
--- a/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py
+++ b/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py
@@ -1,4 +1,7 @@
from django import template
+from django.utils.encoding import smart_unicode, smart_str
+from django.utils.html import conditional_escape
+from django.utils.safestring import mark_safe
register = template.Library()
@@ -11,15 +14,28 @@ try:
except:
colorize = False
-def syntaxhilight(value, arg="diff"):
- '''Returns a syntax-hilighted version of Code; requires code/language arguments'''
+@register.filter
+def syntaxhilight(value, arg="diff", autoescape=None):
+ """
+ Returns a syntax-hilighted version of Code; requires code/language arguments
+ """
+
+ if autoescape:
+ value = conditional_escape(value)
+ arg = conditional_escape(arg)
+
if colorize:
try:
+ output = u'<style type="text/css">' \
+ + smart_unicode(HtmlFormatter().get_style_defs('.highlight')) \
+ + u'</style>'
+
lexer = get_lexer_by_name(arg)
- return highlight(value, lexer, HtmlFormatter())
+ output += highlight(value, lexer, HtmlFormatter())
+ return mark_safe(output)
except:
return value
else:
- return value
+ return mark_safe(u'<div class="note-box">Tip: Install pygments for highlighting</div><pre>%s</pre>' % value)
+syntaxhilight.needs_autoescape = True
-register.filter('syntaxhilight', syntaxhilight)
diff --git a/src/lib/Server/Reports/reports/urls.py b/src/lib/Server/Reports/reports/urls.py
new file mode 100644
index 000000000..9970d26a1
--- /dev/null
+++ b/src/lib/Server/Reports/reports/urls.py
@@ -0,0 +1,55 @@
+from django.conf.urls.defaults import *
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.http import HttpResponsePermanentRedirect
+from Bcfg2.Server.Reports.utils import filteredUrls, paginatedUrls, timeviewUrls
+
+def newRoot(request):
+ try:
+ grid_view = reverse('reports_grid_view')
+ except NoReverseMatch:
+ grid_view = '/grid'
+ return HttpResponsePermanentRedirect(grid_view)
+
+urlpatterns = patterns('Bcfg2.Server.Reports.reports',
+ (r'^$', newRoot),
+
+ url(r'^manage/?$', 'views.client_manage', name='reports_client_manage'),
+ url(r'^client/(?P<hostname>\S+)/(?P<pk>\d+)/?$', 'views.client_detail', name='reports_client_detail_pk'),
+ url(r'^client/(?P<hostname>\S+)/?$', 'views.client_detail', name='reports_client_detail'),
+ url(r'^elements/(?P<type>\w+)/(?P<pk>\d+)/?$', 'views.config_item', name='reports_item'),
+)
+
+urlpatterns += patterns('Bcfg2.Server.Reports.reports',
+ *timeviewUrls(
+ (r'^grid/?$', 'views.client_index', None, 'reports_grid_view'),
+ (r'^summary/?$', 'views.display_summary', None, 'reports_summary'),
+ (r'^timing/?$', 'views.display_timing', None, 'reports_timing'),
+ (r'^elements/(?P<type>\w+)/?$', 'views.config_item_list', None, 'reports_item_list'),
+))
+
+urlpatterns += patterns('Bcfg2.Server.Reports.reports',
+ *filteredUrls(*timeviewUrls(
+ (r'^detailed/?$',
+ 'views.client_detailed_list', None, 'reports_detailed_list')
+)))
+
+urlpatterns += patterns('Bcfg2.Server.Reports.reports',
+ *paginatedUrls( *filteredUrls(
+ (r'^history/?$',
+ 'views.render_history_view', None, 'reports_history'),
+ (r'^history/(?P<hostname>[\w\-\.]+)/?$',
+ 'views.render_history_view', None, 'reports_client_history'),
+)))
+
+ # Uncomment this for admin:
+ #(r'^admin/', include('django.contrib.admin.urls')),
+
+
+## Uncomment this section if using authentication
+#urlpatterns += patterns('',
+# (r'^login/$', 'django.contrib.auth.views.login',
+# {'template_name': 'auth/login.html'}),
+# (r'^logout/$', 'django.contrib.auth.views.logout',
+# {'template_name': 'auth/logout.html'})
+# )
+
diff --git a/src/lib/Server/Reports/reports/views.py b/src/lib/Server/Reports/reports/views.py
index d159dcd43..64617ce70 100644
--- a/src/lib/Server/Reports/reports/views.py
+++ b/src/lib/Server/Reports/reports/views.py
@@ -1,354 +1,379 @@
-# Create your views here.
-#from django.shortcuts import get_object_or_404, render_to_response
-from django.template import Context, loader
-from django.http import HttpResponseRedirect, HttpResponse
+"""
+Report views
+
+Functions to handle all of the reporting views.
+"""
+from django.template import Context, RequestContext, loader
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseServerError, Http404
from django.shortcuts import render_to_response, get_object_or_404
-from Bcfg2.Server.Reports.reports.models import Client, Interaction, Entries, Entries_interactions, Performance, Reason
-from Bcfg2.Server.Reports.reports.models import TYPE_BAD, TYPE_MODIFIED, TYPE_EXTRA
-from datetime import datetime, timedelta
-from time import strptime
+from django.core.urlresolvers import resolve, reverse, Resolver404, NoReverseMatch
from django.db import connection
from django.db.backends import util
-from django.contrib.auth.decorators import login_required
-def index(request):
- return render_to_response('index.html')
+from Bcfg2.Server.Reports.reports.models import *
+from datetime import datetime, timedelta
+from time import strptime
+import sys
-def config_item_modified(request, eyedee =None, timestamp = 'now', type=TYPE_MODIFIED):
- #if eyedee = None, dump with a 404
- timestamp = timestamp.replace("@"," ")
- if type == TYPE_MODIFIED:
- mod_or_bad = "modified"
- else:
- mod_or_bad = "bad"
-
- item = get_object_or_404(Entries_interactions, id=eyedee)
- #if everything is blank except current_exists, do something special
- cursor = connection.cursor()
- if timestamp == 'now':
- cursor.execute("select client_id from reports_interaction, reports_entries_interactions, reports_client "+
- "WHERE reports_client.current_interaction_id = reports_entries_interactions.interaction_id "+
- "AND reports_entries_interactions.interaction_id = reports_interaction.id "+
- "AND reports_entries_interactions.entry_id = %s " +
- "AND reports_entries_interactions.reason_id = %s", [item.entry.id, item.reason.id])
- associated_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()])
- else:
- interact_queryset = Interaction.objects.interaction_per_client(timestamp)
- interactionlist = []
- [interactionlist.append(x.id) for x in interact_queryset]
- if not interactionlist == []:
- cursor.execute("select client_id from reports_interaction, reports_entries_interactions, reports_client "+
- "WHERE reports_entries_interactions.interaction_id IN %s "+
- "AND reports_entries_interactions.interaction_id = reports_interaction.id "+
- "AND reports_entries_interactions.entry_id = %s " +
- "AND reports_entries_interactions.reason_id = %s ", [interactionlist, item.entry_id, item.reason.id])
- associated_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()])
- else:
- associated_client_list = []
+class PaginationError(Exception):
+ """This error is raised when pagination cannot be completed."""
+ pass
- if timestamp == 'now':
- timestamp = datetime.now().isoformat('@')
+def server_error(request):
+ """
+ 500 error handler.
- return render_to_response('config_items/index.html', {'item':item,
- 'mod_or_bad':mod_or_bad,
- 'associated_client_list':associated_client_list,
- 'timestamp' : timestamp,
- 'timestamp_date' : timestamp[:10],
- 'timestamp_time' : timestamp[11:19]})
+ For now always return the debug response. Mailing isn't appropriate here.
+ """
+ from django.views import debug
+ return debug.technical_500_response(request, *sys.exc_info())
-def config_item_bad(request, eyedee = None, timestamp = 'now'):
- return config_item_modified(request, eyedee, timestamp, TYPE_BAD)
+def timeview(fn):
+ """
+ Setup a timeview view
-def bad_item_index(request, timestamp = 'now', type=TYPE_BAD):
- timestamp = timestamp.replace("@"," ")
- if type == TYPE_BAD:
- mod_or_bad = "bad"
- else:
- mod_or_bad = "modified"
+ Handles backend posts from the calendar and converts date pieces
+ into a 'timestamp' parameter
+
+ """
+ def _handle_timeview(request, **kwargs):
+ """Send any posts back."""
+ if request.method == 'POST':
+ cal_date = request.POST['cal_date']
+ try:
+ fmt = "%Y/%m/%d"
+ if cal_date.find(' ') > -1:
+ fmt += " %H:%M"
+ timestamp = datetime(*strptime(cal_date, fmt)[0:6])
+ view, args, kw = resolve(request.path)
+ kw['year'] = "%0.4d" % timestamp.year
+ kw['month'] = "%02.d" % timestamp.month
+ kw['day'] = "%02.d" % timestamp.day
+ if cal_date.find(' ') > -1:
+ kw['hour'] = timestamp.hour
+ kw['minute'] = timestamp.minute
+ return HttpResponseRedirect(reverse(view, args=args, kwargs=kw))
+ except KeyError:
+ pass
+ except:
+ pass
+ # FIXME - Handle this
+
+ """Extract timestamp from args."""
+ timestamp = None
+ try:
+ timestamp = datetime(int(kwargs.pop('year')), int(kwargs.pop('month')),
+ int(kwargs.pop('day')), int(kwargs.pop('hour', 0)),
+ int(kwargs.pop('minute', 0)), 0)
+ kwargs['timestamp'] = timestamp
+ except KeyError:
+ pass
+ except:
+ raise
+ return fn(request, **kwargs)
+
+ return _handle_timeview
+
+def config_item(request, pk, type="bad"):
+ """
+ Display a single entry.
+
+ Dispalys information about a single entry.
+
+ """
+ item = get_object_or_404(Entries_interactions, id=pk)
+ timestamp=item.interaction.timestamp
+ time_start=item.interaction.timestamp.replace(\
+ hour=0, minute=0, second=0, microsecond=0)
+ time_end=time_start + timedelta(days=1)
+
+ todays_data = Interaction.objects.filter(\
+ timestamp__gte=time_start,\
+ timestamp__lt=time_end)
+ shared_entries = Entries_interactions.objects.filter(entry=item.entry,\
+ reason=item.reason, type=item.type,
+ interaction__in=[x['id']\
+ for x in todays_data.values('id')])
+
+ associated_list = Interaction.objects.filter(id__in=[x['interaction']\
+ for x in shared_entries.values('interaction')])\
+ .order_by('client__name','timestamp').select_related().all()
+
+ return render_to_response('config_items/item.html',
+ {'item':item,
+ 'isextra': item.type == TYPE_EXTRA,
+ 'mod_or_bad': type,
+ 'associated_list':associated_list,
+ 'timestamp' : timestamp},
+ context_instance=RequestContext(request))
+
+@timeview
+def config_item_list(request, type, timestamp=None):
+ """Render a listing of affected elements"""
+ mod_or_bad = type.lower()
+ type = convert_entry_type_to_id(type)
+ if type < 0:
+ raise Http404
- current_clients = [c.current_interaction
- for c in Client.objects.active(timestamp)]
+ current_clients = Interaction.objects.get_interaction_per_client_ids(timestamp)
item_list_dict = {}
- for x in Entries_interactions.objects.select_related().filter(interaction__in=current_clients, type=type).distinct():
+ seen = dict()
+ for x in Entries_interactions.objects.filter(interaction__in=current_clients, type=type).select_related():
+ if (x.entry, x.reason) in seen:
+ continue
+ seen[(x.entry, x.reason)] = 1
if item_list_dict.get(x.entry.kind, None):
item_list_dict[x.entry.kind].append(x)
else:
item_list_dict[x.entry.kind] = [x]
- item_list_pseudodict = item_list_dict.items()
- if timestamp == 'now':
- timestamp = datetime.now().isoformat('@')
+ for kind in item_list_dict:
+ item_list_dict[kind].sort(lambda a,b: cmp(a.entry.name, b.entry.name))
- return render_to_response('config_items/listing.html', {'item_list_pseudodict':item_list_pseudodict,
+ return render_to_response('config_items/listing.html', {'item_list_dict':item_list_dict,
'mod_or_bad':mod_or_bad,
- 'timestamp' : timestamp,
- 'timestamp_date' : timestamp[:10],
- 'timestamp_time' : timestamp[11:19]})
-def modified_item_index(request, timestamp = 'now'):
- return bad_item_index(request, timestamp, TYPE_MODIFIED)
-
-def client_index(request, timestamp = 'now'):
- timestamp = timestamp.replace("@"," ")
-
- c_dict = dict()
- [c_dict.__setitem__(cl.id,cl.name) for cl in Client.objects.active(timestamp).order_by('name')]
-
- list = []
- for inter in Interaction.objects.interaction_per_client(timestamp):
- if inter.client_id in c_dict:
- list.append([c_dict[inter.client_id], inter])
- list.sort(lambda a,b: cmp(a[0], b[0]))
- half_list = len(list) / 2
-
- if timestamp == 'now':
- timestamp = datetime.now().isoformat('@')
+ 'timestamp' : timestamp},
+ context_instance=RequestContext(request))
+
+@timeview
+def client_index(request, timestamp=None):
+ """
+ Render a grid view of active clients.
+
+ Keyword parameters:
+ timestamp -- datetime objectto render from
+
+ """
+ list = Interaction.objects.interaction_per_client(timestamp).select_related()\
+ .order_by("client__name").all()
+
return render_to_response('clients/index.html',
- {'inter_list': list,
- 'half_list': half_list,
- 'timestamp' : timestamp,
- 'timestamp_date' : timestamp[:10],
- 'timestamp_time' : timestamp[11:19]})
+ { 'inter_list': list, 'timestamp' : timestamp},
+ context_instance=RequestContext(request))
-def client_detailed_list(request, **kwargs):
+@timeview
+def client_detailed_list(request, timestamp=None, **kwargs):
"""
Provides a more detailed list view of the clients. Allows for extra
- filters to be passed in. Somewhat clunky now that dates are allowed.
+ filters to be passed in.
"""
- context = dict(path=request.path)
- timestamp = 'now'
- entry_max = datetime.now()
- if request.GET:
- context['qsa']='?%s' % request.GET.urlencode()
- if request.GET.has_key('date1') and request.GET.has_key('time'):
- timestamp = "%s %s" % (request.GET['date1'],request.GET['time'])
- entry_max = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6])
- client_list = Client.objects.active(timestamp).order_by('name')
- if timestamp == 'now':
- timestamp = datetime.now().isoformat('@')
- context['timestamp_date'] = timestamp[:10]
- context['timestamp_time'] = timestamp[11:19]
-
- interactions = Interaction.objects.interaction_per_client(timestamp)
- if 'state' in kwargs and kwargs['state']:
- context['state'] = kwargs['state']
- interactions=interactions.filter(state__exact=kwargs['state'])
- if 'server' in kwargs and kwargs['server']:
- interactions=interactions.filter(server__exact=kwargs['server'])
- context['server'] = kwargs['server']
- # build the entry list from available clients
- c_dict = dict()
- [c_dict.__setitem__(cl.id,cl.name) for cl in client_list]
-
- entry_list = []
- for inter in interactions:
- if inter.client_id in c_dict:
- entry_list.append([c_dict[inter.client_id], inter, \
- entry_max - inter.timestamp > timedelta(hours=24)])
- entry_list.sort(lambda a,b: cmp(a[0], b[0]))
- '''
- if(datetime.now()-self.timestamp > timedelta(hours=25) ):
- return True
- else:
- return False
- '''
-
- context['entry_list'] = entry_list
- return render_to_response('clients/detailed-list.html', context)
+ kwargs['interaction_base'] = Interaction.objects.interaction_per_client(timestamp).select_related()
+ kwargs['orderby'] = "client__name"
+ kwargs['page_limit'] = 0
+ return render_history_view(request, 'clients/detailed-list.html', **kwargs)
def client_detail(request, hostname = None, pk = None):
- #SETUP error pages for when you specify a client or interaction that doesn't exist
+ context = dict()
client = get_object_or_404(Client, name=hostname)
if(pk == None):
- interaction = client.current_interaction
+ context['interaction'] = client.current_interaction
+ return render_history_view(request, 'clients/detail.html', page_limit=5,
+ client=client, context=context)
else:
- interaction = client.interactions.get(pk=pk)#can this be a get object or 404?
- return render_to_response('clients/detail.html', {'client': client, 'interaction': interaction})
+ context['interaction'] = client.interactions.get(pk=pk)
+ return render_history_view(request, 'clients/detail.html', page_limit=5,
+ client=client, maxdate=context['interaction'].timestamp, context=context)
-def client_manage(request, hostname = None):
- #SETUP error pages for when you specify a client or interaction that doesn't exist
- client = get_object_or_404(Client, name=hostname)
- currenttime = datetime.now().isoformat('@')
- if client.expiration != None:
- message = ("This client currently has an expiration date of %s. "
- "Reports after %s will not include data for this host "
- "You may change this if you wish by selecting a new "
- "time, earlier or later."
- % (client.expiration, client.expiration))
- else:
- message = ("This client is currently active and displayed. You "
- "may choose a date after which this client will no "
- "longer appear in reports.")
+def client_manage(request):
+ """Manage client expiration"""
+ message = ''
if request.method == 'POST':
- date = request.POST['date1']
- time = request.POST['time']
try:
- timestamp = datetime(*(strptime(date+"@"+time, "%Y-%m-%d@%H:%M:%S")[0:6]))
- except ValueError:
- timestamp = None
- if timestamp == None:
- message = "Invalid removal date, please try again using the format: yyyy-mm-dd hh:mm:ss."
- else:
- client.expiration = timestamp
+ client_name = request.POST.get('client_name', None)
+ client_action = request.POST.get('client_action', None)
+ client = Client.objects.get(name=client_name)
+ if client_action == 'expire':
+ client.expiration = datetime.now();
+ client.save()
+ message = "Expiration for %s set to %s." % \
+ (client_name, client.expiration.strftime("%Y-%m-%d %H:%M:%S"))
+ elif client_action == 'unexpire':
+ client.expiration = None;
client.save()
- message = "Expiration for client set to %s." % client.expiration
- return render_to_response('clients/manage.html', {'client': client, 'message': message,
- 'timestamp_date' : currenttime[:10],
- 'timestamp_time' : currenttime[11:19]})
-
-def display_sys_view(request, timestamp = 'now'):
- client_lists = prepare_client_lists(request, timestamp)
- return render_to_response('displays/sys_view.html', client_lists)
-
-def display_summary(request, timestamp = 'now'):
-
- client_lists = prepare_client_lists(request, timestamp)
- #this returns timestamp and the timestamp parts too
- return render_to_response('displays/summary.html', client_lists)
-
-def display_timing(request, timestamp = 'now'):
- #We're going to send a list of dictionaries. Each dictionary will be a row in the table
- #+------+-------+----------------+-----------+---------+----------------+-------+
- #| name | parse | probe download | inventory | install | cfg dl & parse | total |
- #+------+-------+----------------+-----------+---------+----------------+-------+
- client_list = Client.objects.active(timestamp.replace("@"," ")).order_by('name')
- stats_list = []
-
- if not timestamp == 'now':
- results = Performance.objects.performance_per_client(timestamp.replace("@"," "))
+ message = "%s is now active." % client_name
else:
- results = Performance.objects.performance_per_client()
- timestamp = datetime.now().isoformat('@')
-
- for client in client_list:#Go explicitly to an interaction ID! (new item in dictionary)
+ message = "Missing action"
+ except Client.DoesNotExist:
+ if not client_name:
+ client_name = "<none>"
+ message = "Couldn't find client \"%s\"" % client_name
+
+ return render_to_response('clients/manage.html',
+ {'clients': Client.objects.order_by('name').all(), 'message': message},
+ context_instance=RequestContext(request))
+
+@timeview
+def display_summary(request, timestamp=None):
+ """
+ Display a summary of the bcfg2 world
+ """
+ query = Interaction.objects.interaction_per_client(timestamp).select_related()
+ node_count = query.count()
+ recent_data = query.all()
+ if not timestamp:
+ timestamp = datetime.now()
+
+ collected_data = dict(clean=[],bad=[],modified=[],extra=[],stale=[],pings=[])
+ for node in recent_data:
+ if timestamp - node.timestamp > timedelta(hours=24):
+ collected_data['stale'].append(node)
+ # If stale check for uptime
try:
- d = results[client.name]
- except KeyError:
- d = {}
+ if node.client.pings.latest().status == 'N':
+ collected_data['pings'].append(node)
+ except Ping.DoesNotExist:
+ collected_data['pings'].append(node)
+ continue
+ if node.bad_entry_count() > 0:
+ collected_data['bad'].append(node)
+ else:
+ collected_data['clean'].append(node)
+ if node.modified_entry_count() > 0:
+ collected_data['modified'].append(node)
+ if node.extra_entry_count() > 0:
+ collected_data['extra'].append(node)
+
+ # label, header_text, node_list
+ summary_data = []
+ get_dict = lambda name, label: { 'name': name,
+ 'nodes': collected_data[name],
+ 'label': label }
+ if len(collected_data['clean']) > 0:
+ summary_data.append( get_dict('clean', 'nodes are clean.') )
+ if len(collected_data['bad']) > 0:
+ summary_data.append( get_dict('bad', 'nodes are bad.') )
+ if len(collected_data['modified']) > 0:
+ summary_data.append( get_dict('modified', 'nodes were modified.') )
+ if len(collected_data['extra']) > 0:
+ summary_data.append( get_dict('extra',
+ 'nodes have extra configurations.') )
+ if len(collected_data['stale']) > 0:
+ summary_data.append( get_dict('stale',
+ 'nodes did not run within the last 24 hours.') )
+ if len(collected_data['pings']) > 0:
+ summary_data.append( get_dict('pings',
+ 'are down.') )
+
+ return render_to_response('displays/summary.html',
+ {'summary_data': summary_data, 'node_count': node_count,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+@timeview
+def display_timing(request, timestamp=None):
+ mdict = dict()
+ inters = Interaction.objects.interaction_per_client(timestamp).select_related().all()
+ [mdict.__setitem__(inter, {'name': inter.client.name}) \
+ for inter in inters]
+ for metric in Performance.objects.filter(interaction__in=mdict.keys()).all():
+ for i in metric.interaction.all():
+ mdict[i][metric.metric] = metric.value
+ return render_to_response('displays/timing.html',
+ {'metrics': mdict.values(), 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+def render_history_view(request, template='clients/history.html', **kwargs):
+ """
+ Provides a detailed history of a clients interactions.
+
+ Renders a detailed history of a clients interactions. Allows for various
+ filters and settings. Automatically sets pagination data into the context.
+
+ Keyword arguments:
+ interaction_base -- Interaction QuerySet to build on
+ (default Interaction.objects)
+ context -- Additional context data to render with
+ page_number -- Page to display (default 1)
+ page_limit -- Number of results per page, if 0 show all (default 25)
+ client -- Client object to render
+ hostname -- Client hostname to lookup and render. Returns a 404 if
+ not found
+ server -- Filter interactions by server
+ state -- Filter interactions by state
+ entry_max -- Most recent interaction to display
+ orderby -- Sort results using this field
- dict_unit = {}
- try:
- dict_unit["name"] = client.name #node name
- except:
- dict_unit["name"] = "n/a"
- try:
- dict_unit["parse"] = round(d["config_parse"] - d["config_download"], 4) #parse
- except:
- dict_unit["parse"] = "n/a"
- try:
- dict_unit["probe"] = round(d["probe_upload"] - d["start"], 4) #probe
- except:
- dict_unit["probe"] = "n/a"
- try:
- dict_unit["inventory"] = round(d["inventory"] - d["initialization"], 4) #inventory
- except:
- dict_unit["inventory"] = "n/a"
- try:
- dict_unit["install"] = round(d["install"] - d["inventory"], 4) #install
- except:
- dict_unit["install"] = "n/a"
- try:
- dict_unit["config"] = round(d["config_parse"] - d["probe_upload"], 4)#config download & parse
- except:
- dict_unit["config"] = "n/a"
+ """
+
+ context = kwargs.get('context', dict())
+ max_results = int(kwargs.get('page_limit', 25))
+ page = int(kwargs.get('page_number', 1))
+
+ client=kwargs.get('client', None)
+ if not client and 'hostname' in kwargs:
+ client = get_object_or_404(Client, name=kwargs['hostname'])
+ if client:
+ context['client'] = client
+
+ entry_max = kwargs.get('maxdate', None)
+ context['entry_max'] = entry_max
+
+ # Either filter by client or limit by clients
+ iquery = kwargs.get('interaction_base', Interaction.objects)
+ if client:
+ iquery = iquery.filter(client__exact=client).select_related()
+
+ if 'orderby' in kwargs and kwargs['orderby']:
+ iquery = iquery.order_by(kwargs['orderby'])
+
+ if 'state' in kwargs and kwargs['state']:
+ iquery = iquery.filter(state__exact=kwargs['state'])
+ if 'server' in kwargs and kwargs['server']:
+ iquery = iquery.filter(server__exact=kwargs['server'])
+
+ if entry_max:
+ iquery = iquery.filter(timestamp__lte=entry_max)
+
+ if max_results < 0:
+ max_results = 1
+ entry_list = []
+ if max_results > 0:
try:
- dict_unit["total"] = round(d["finished"] - d["start"], 4) #total
- except:
- dict_unit["total"] = "n/a"
-
- stats_list.append(dict_unit)
-
- return render_to_response('displays/timing.html', {'client_list': client_list,
- 'stats_list': stats_list,
- 'timestamp' : timestamp,
- 'timestamp_date' : timestamp[:10],
- 'timestamp_time' : timestamp[11:19]})
-
-def display_index(request):
- return render_to_response('displays/index.html')
-
-def prepare_client_lists(request, timestamp = 'now'):
- #I suggest we implement "expiration" here.
-
- timestamp = timestamp.replace("@"," ")
- #client_list = Client.objects.all().order_by('name')#change this to order by interaction's state
- client_interaction_dict = {}
- clean_client_list = []
- bad_client_list = []
- extra_client_list = []
- modified_client_list = []
- stale_up_client_list = []
- #stale_all_client_list = []
- down_client_list = []
-
- cursor = connection.cursor()
-
- interact_queryset = Interaction.objects.interaction_per_client(timestamp)
- # or you can specify a time like this: '2007-01-01 00:00:00'
- [client_interaction_dict.__setitem__(x.client_id, x) for x in interact_queryset]
- client_list = Client.objects.active(timestamp).filter(id__in=client_interaction_dict.keys()).order_by('name')
-
- [clean_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=[y.client_id for y in interact_queryset.filter(state='clean')])]
- [bad_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=[y.client_id for y in interact_queryset.filter(state='dirty')])]
-
- client_ping_dict = {}
- [client_ping_dict.__setitem__(x,'Y') for x in client_interaction_dict.keys()]#unless we know otherwise...
+ rec_start, rec_end = prepare_paginated_list(request, context, iquery, page, max_results)
+ except PaginationError, page_error:
+ if isinstance(page_error[0], HttpResponse):
+ return page_error[0]
+ return HttpResponseServerError(page_error)
+ context['entry_list'] = iquery.all()[rec_start:rec_end]
+ else:
+ context['entry_list'] = iquery.all()
+
+ return render_to_response(template, context,
+ context_instance=RequestContext(request))
+
+def prepare_paginated_list(request, context, paged_list, page=1, max_results=25):
+ """
+ Prepare context and slice an object for pagination.
+ """
+ if max_results < 1:
+ raise PaginationError, "Max results less then 1"
+ if paged_list == None:
+ raise PaginationError, "Invalid object"
+
+ try:
+ nitems = paged_list.count()
+ except TypeError:
+ nitems = len(paged_list)
+ rec_start = (page - 1) * int(max_results)
try:
- cursor.execute("select reports_ping.status, x.client_id from (select client_id, MAX(endtime) "+
- "as timer from reports_ping GROUP BY client_id) x, reports_ping where "+
- "reports_ping.client_id = x.client_id AND reports_ping.endtime = x.timer")
- [client_ping_dict.__setitem__(x[1], x[0]) for x in cursor.fetchall()]
+ total_pages = (nitems / int(max_results)) + 1
except:
- pass #This is to fix problems when you have only zero records returned
-
- client_down_ids = [y for y in client_ping_dict.keys() if client_ping_dict[y]=='N']
- if not client_down_ids == []:
- [down_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=client_down_ids)]
-
- if (timestamp == 'now' or timestamp == None):
- cursor.execute("select client_id, MAX(timestamp) as timestamp from reports_interaction GROUP BY client_id")
- results = cursor.fetchall()
- for x in results:
- if type(x[1]) == type("") or type(x[1]) == type(u""):
- ts = util.typecast_timestamp(x[1])
- else:
- ts = x[1]
- stale_all_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in results if datetime.now() - ts > timedelta(days=1)])
- else:
- cursor.execute("select client_id, timestamp, MAX(timestamp) as timestamp from reports_interaction "+
- "WHERE timestamp < %s GROUP BY client_id", [timestamp])
- t = strptime(timestamp,"%Y-%m-%d %H:%M:%S")
- datetimestamp = datetime(t[0], t[1], t[2], t[3], t[4], t[5])
- results = cursor.fetchall()
- for x in results:
- if type(x[1]) == type(""):
- x[1] = util.typecast_timestamp(x[1])
- stale_all_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in results if datetimestamp - x[1] > timedelta(days=1)])
-
- [stale_up_client_list.append(x) for x in stale_all_client_list if not client_ping_dict[x.id]=='N']
+ total_pages = 1
+ if page > total_pages:
+ # If we passed beyond the end send back
+ try:
+ view, args, kwargs = resolve(request.path)
+ kwargs['page_number'] = total_pages
+ raise PaginationError, HttpResponseRedirect( reverse(view, kwargs=kwargs) )
+ except (Resolver404, NoReverseMatch, ValueError):
+ raise "Accessing beyond last page. Unable to resolve redirect."
+
+ context['total_pages'] = total_pages
+ context['records_per_page'] = max_results
+ return (rec_start, rec_start + int(max_results))
-
- cursor.execute("SELECT reports_client.id FROM reports_client, reports_interaction, reports_entries_interactions WHERE reports_client.id = reports_interaction.client_id AND reports_client.current_interaction_id = reports_entries_interactions.interaction_id and reports_entries_interactions.type=%s GROUP BY reports_client.id", [TYPE_MODIFIED])
- modified_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()])
-
- cursor.execute("SELECT reports_client.id FROM reports_client, reports_interaction, reports_entries_interactions WHERE reports_client.id = reports_interaction.client_id AND reports_client.current_interaction_id = reports_entries_interactions.interaction_id and reports_entries_interactions.type=%s GROUP BY reports_client.id", [TYPE_EXTRA])
- extra_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()])
-
- if timestamp == 'now':
- timestamp = datetime.now().isoformat('@')
-
- return {'client_list': client_list,
- 'client_interaction_dict':client_interaction_dict,
- 'clean_client_list': clean_client_list,
- 'bad_client_list': bad_client_list,
- 'extra_client_list': extra_client_list,
- 'modified_client_list': modified_client_list,
- 'stale_up_client_list': stale_up_client_list,
- 'stale_all_client_list': stale_all_client_list,
- 'down_client_list': down_client_list,
- 'timestamp' : timestamp,
- 'timestamp_date' : timestamp[:10],
- 'timestamp_time' : timestamp[11:19]}
diff --git a/src/lib/Server/Reports/settings.py b/src/lib/Server/Reports/settings.py
index 59d29114d..81220c0e3 100644
--- a/src/lib/Server/Reports/settings.py
+++ b/src/lib/Server/Reports/settings.py
@@ -1,3 +1,5 @@
+import django
+
# Django settings for bcfg2 reports project.
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
c = ConfigParser()
@@ -62,7 +64,9 @@ MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT.
# Example: "http://media.lawrence.com"
-MEDIA_URL = ''
+MEDIA_URL = '/site_media'
+if c.has_option('statistics', 'web_prefix'):
+ MEDIA_URL = c.get('statistics', 'web_prefix').rstrip('/') + MEDIA_URL
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
@@ -109,9 +113,26 @@ TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates".
# Always use forward slashes, even on Windows.
'/usr/share/python-support/python-django/django/contrib/admin/templates/',
- '/usr/share/bcfg2/Reports/templates'
+ 'Bcfg2.Server.Reports.reports'
)
+if django.VERSION[0] == 1 and django.VERSION[1] < 2:
+ TEMPLATE_CONTEXT_PROCESSORS = (
+ 'django.core.context_processors.auth',
+ 'django.core.context_processors.debug',
+ 'django.core.context_processors.i18n',
+ 'django.core.context_processors.media',
+ 'django.core.context_processors.request'
+ )
+else:
+ 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.core.context_processors.request'
+ )
+
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
diff --git a/src/lib/Server/Reports/urls.py b/src/lib/Server/Reports/urls.py
index e1326b5ea..5d298c974 100644
--- a/src/lib/Server/Reports/urls.py
+++ b/src/lib/Server/Reports/urls.py
@@ -1,4 +1,7 @@
from django.conf.urls.defaults import *
+from django.http import HttpResponsePermanentRedirect
+
+handler500 = 'Bcfg2.Server.Reports.reports.views.server_error'
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
c = ConfigParser()
@@ -9,56 +12,15 @@ c.read(['/etc/bcfg2.conf', '/etc/bcfg2-web.conf'])
# web_prefix_root is a workaround for the index
if c.has_option('statistics', 'web_prefix'):
web_prefix = c.get('statistics', 'web_prefix').lstrip('/')
- web_prefix_root = web_prefix
else:
web_prefix = ''
- web_prefix_root = '/'
urlpatterns = patterns('',
- # Example:
- # (r'^%sBcfg2.Server.Reports/' % web_prefix, include('Bcfg2.Server.Reports.apps.foo.urls.foo')),
- (r'^%s*$' % web_prefix_root,'Bcfg2.Server.Reports.reports.views.index'),
-
- (r'^%sclients-detailed/state/(?P<state>\w+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'),
- (r'^%sclients-detailed/server/(?P<server>[\w\-\.]+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'),
- (r'^%sclients-detailed/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'),
- (r'^%sclients-detailed/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'),
- (r'^%sclients/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'),
- (r'^%sclients/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'),
- (r'^%sclients/(?P<hostname>\S+)/(?P<pk>\d+)/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'),
- (r'^%sclients/(?P<hostname>\S+)/manage/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_manage'),
- (r'^%sclients/(?P<hostname>\S+)/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'),
- (r'^%sclients/(?P<hostname>\S+)$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'),
- #hack because hostnames have periods and we still want to append slash
- (r'^%sclients/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'),
- (r'^%sdisplays/sys-view/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_sys_view'),
- (r'^%sdisplays/sys-view/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_sys_view'),
- (r'^%sdisplays/summary/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_summary'),
- (r'^%sdisplays/summary/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_summary'),
- (r'^%sdisplays/timing/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_timing'),
- (r'^%sdisplays/timing/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_timing'),
- (r'^%sdisplays/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_index'),
-
- (r'^%selements/modified/(?P<eyedee>\d+)/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_modified'),
- (r'^%selements/modified/(?P<eyedee>\d+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_modified'),
- (r'^%selements/modified/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01]\
- [0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.modified_item_index'),
- (r'^%selements/modified/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.modified_item_index'),
- (r'^%selements/bad/(?P<eyedee>\d+)/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_bad'),
- (r'^%selements/bad/(?P<eyedee>\d+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_bad'),
- (r'^%selements/bad/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01]\
- [0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.bad_item_index'),
- (r'^%selements/bad/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.bad_item_index'),
+ (r'^%s' % web_prefix, include('Bcfg2.Server.Reports.reports.urls'))
)
- # Uncomment this for admin:
- #(r'^%sadmin/' % web_prefix, include('django.contrib.admin.urls')),
-
-
-## Uncomment this section if using authentication
-#urlpatterns += patterns('',
-# (r'^%slogin/$' % web_prefix, 'django.contrib.auth.views.login',
-# {'template_name': 'auth/login.html'}),
-# (r'^%slogout/$' % web_prefix, 'django.contrib.auth.views.logout',
-# {'template_name': 'auth/logout.html'})
-# )
+urlpatterns += patterns("django.views",
+ url(r"media/(?P<path>.*)$", "static.serve", {
+ "document_root": '/Users/tlaszlo/svn/bcfg2/reports/site_media/',
+ })
+)
diff --git a/src/lib/Server/Reports/utils.py b/src/lib/Server/Reports/utils.py
index 2ef21e446..b74f09e74 100755
--- a/src/lib/Server/Reports/utils.py
+++ b/src/lib/Server/Reports/utils.py
@@ -1,7 +1,13 @@
-'''Helper functions for reports'''
+"""Helper functions for reports"""
+from Bcfg2.Server.Reports.reports.models import TYPE_CHOICES
+from django.conf.urls.defaults import *
+import re
+
+"""List of filters provided by filteredUrls"""
+filter_list = ('server', 'state')
class BatchFetch(object):
- '''Fetch Django objects in smaller batches to save memory'''
+ """Fetch Django objects in smaller batches to save memory"""
def __init__(self, obj, step=10000):
self.count = 0
@@ -15,8 +21,8 @@ class BatchFetch(object):
return self
def next(self):
- '''Return the next object from our array and fetch from the
- database when needed'''
+ """Return the next object from our array and fetch from the
+ database when needed"""
if self.block_count + self.count - self.step == self.max:
raise StopIteration
if self.block_count == 0 or self.count == self.step:
@@ -28,3 +34,83 @@ class BatchFetch(object):
self.count += 1
return self.data[self.count - 1]
+def generateUrls(fn):
+ """
+ Parse url tuples and send to functions.
+
+ Decorator for url generators. Handles url tuple parsing
+ before the actual function is called.
+ """
+ def url_gen(*urls):
+ results = []
+ for url_tuple in urls:
+ if isinstance(url_tuple, (list, tuple)):
+ results += fn(*url_tuple)
+ else:
+ raise ValueError("Unable to handle compiled urls")
+ return results
+ return url_gen
+
+@generateUrls
+def paginatedUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a group of url tuples and adds paginated urls.
+
+ Extends a url tuple to include paginated urls. Currently doesn't handle url() compiled
+ patterns.
+
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ results += [(pattern + "/(?P<page_number>\d+)" + tail, view, kwargs)]
+ results += [(pattern + "/(?P<page_number>\d+)\|(?P<page_limit>\d+)" + tail, view, kwargs)]
+ if not kwargs:
+ kwargs = dict()
+ kwargs['page_limit'] = 0
+ results += [(pattern + "/?\|(?P<page_limit>all)" + tail, view, kwargs)]
+ return results
+
+@generateUrls
+def filteredUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a url and adds filtered urls.
+
+ Extends a url tuple to include filtered view urls. Currently doesn't
+ handle url() compiled patterns.
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ for filter in ('/state/(?P<state>\w+)',
+ '/server/(?P<server>[\w\-\.]+)',
+ '/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)'):
+ results += [(pattern + filter + tail, view, kwargs)]
+ return results
+
+@generateUrls
+def timeviewUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a url and adds timeview urls
+
+ Extends a url tuple to include filtered view urls. Currently doesn't
+ handle url() compiled patterns.
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ for filter in ('/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/' + \
+ '(?P<hour>\d\d)-(?P<minute>\d\d)',
+ '/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'):
+ results += [(pattern + filter + tail, view, kwargs)]
+ return results
+