diff --git a/TODO.md b/TODO.md
index 4a01a5ab..9c17d763 100644
--- a/TODO.md
+++ b/TODO.md
@@ -400,8 +400,6 @@ serailzer self.instance on create.
* backendLog store method and language... and use it for display_script with correct lexer
-# Compute Resource Data history from Monitor Data.
-
@register.filter
def comma(value):
value = str(value)
@@ -431,13 +429,11 @@ Case
# pre-bill confirmation: remove account if lines.count() == 0 ?
# Discount prepaid metric should be more optimal https://orchestra.pangea.org/admin/orders/order/40/
+# -> order.billed_metric besides billed_until
+
+# USE CONTROLLED MONITOR GRAPHS
+# graph metric storage
+# CLEANUP MONITOR DATA 0.00 (from SUM aggregators, AVG are needed for maintaining state), maybe also aggregate older values
+# MULTIPLE GRAPH TYPE: datapoint line chart for AVG, stacked for SUM())
-# Resource history, show backend+content_object accumulation stacked bar chart
-# Resource consumption summary: combine selected rather than make individual summaries
-# Search on resource consumption (store content_object str)
-# monitor data value convert to scale and include units and title="data.value"
-# resource history merge multiple monitors, split in multiple graphs different resources and account aggregated resource if multiple objects are selected
-
-
-# order.billed_metric besides billed_until
diff --git a/orchestra/api/options.py b/orchestra/api/options.py
index 060121ef..09150bd6 100644
--- a/orchestra/api/options.py
+++ b/orchestra/api/options.py
@@ -82,13 +82,10 @@ class LinkHeaderRouter(DefaultRouter):
def insert(self, prefix_or_model, name, field, **kwargs):
""" Dynamically add new fields to an existing serializer """
viewset = self.get_viewset(prefix_or_model)
-# setattr(viewset, 'inserted', getattr(viewset, 'inserted', []))
if viewset.serializer_class is None:
viewset.serializer_class = viewset().get_serializer_class()
viewset.serializer_class._declared_fields.update({name: field(**kwargs)})
-# if not name in viewset.inserted:
viewset.serializer_class.Meta.fields += (name,)
-# viewset.inserted.append(name)
# Create a router and register our viewsets with it.
diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py
index db328b1e..75b7857d 100644
--- a/orchestra/contrib/domains/admin.py
+++ b/orchestra/contrib/domains/admin.py
@@ -56,7 +56,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
inlines = (RecordInline, DomainInline)
list_filter = (TopDomainListFilter,)
change_readonly_fields = ('name', 'serial')
- search_fields = ('name', 'account__username')
+ search_fields = ('name', 'account__username', 'records__value')
add_form = BatchDomainCreationAdminForm
actions = (edit_records, set_soa, list_accounts)
change_view_actions = (view_zone, edit_records)
diff --git a/orchestra/contrib/lists/admin.py b/orchestra/contrib/lists/admin.py
index 9f5e8e90..0ddc1cc8 100644
--- a/orchestra/contrib/lists/admin.py
+++ b/orchestra/contrib/lists/admin.py
@@ -64,6 +64,12 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
url(r'^(\d+)/password/$',
self.admin_site.admin_view(useradmin.user_change_password))
] + super(ListAdmin, self).get_urls()
+
+ def save_model(self, request, obj, form, change):
+ """ set password """
+ if not change:
+ obj.set_password(form.cleaned_data["password1"])
+ super(ListAdmin, self).save_model(request, obj, form, change)
admin.site.register(List, ListAdmin)
diff --git a/orchestra/contrib/miscellaneous/migrations/0002_auto_20150723_1252.py b/orchestra/contrib/miscellaneous/migrations/0002_auto_20150723_1252.py
new file mode 100644
index 00000000..b7f76231
--- /dev/null
+++ b/orchestra/contrib/miscellaneous/migrations/0002_auto_20150723_1252.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import orchestra.models.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('miscellaneous', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='miscellaneous',
+ name='identifier',
+ field=orchestra.models.fields.NullableCharField(db_index=True, unique=True, help_text='A unique identifier for this service.', null=True, max_length=256, verbose_name='identifier'),
+ ),
+ ]
diff --git a/orchestra/contrib/miscellaneous/models.py b/orchestra/contrib/miscellaneous/models.py
index f3233133..eb4b8c86 100644
--- a/orchestra/contrib/miscellaneous/models.py
+++ b/orchestra/contrib/miscellaneous/models.py
@@ -39,7 +39,7 @@ class Miscellaneous(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='miscellaneous')
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
- help_text=_("A unique identifier for this service."))
+ db_index=True, help_text=_("A unique identifier for this service."))
description = models.TextField(_("description"), blank=True)
amount = models.PositiveIntegerField(_("amount"), default=1)
is_active = models.BooleanField(_("active"), default=True,
diff --git a/orchestra/contrib/orders/models.py b/orchestra/contrib/orders/models.py
index 11ee27e3..da7a0348 100644
--- a/orchestra/contrib/orders/models.py
+++ b/orchestra/contrib/orders/models.py
@@ -114,6 +114,7 @@ class Order(models.Model):
related_name='orders')
registered_on = models.DateField(_("registered"), default=timezone.now, db_index=True)
cancelled_on = models.DateField(_("cancelled"), null=True, blank=True)
+ # TODO billed metric
billed_on = models.DateField(_("billed"), null=True, blank=True)
billed_until = models.DateField(_("billed until"), null=True, blank=True)
ignore = models.BooleanField(_("ignore"), default=False)
diff --git a/orchestra/contrib/resources/actions.py b/orchestra/contrib/resources/actions.py
index 700becdf..97933254 100644
--- a/orchestra/contrib/resources/actions.py
+++ b/orchestra/contrib/resources/actions.py
@@ -43,90 +43,8 @@ run_monitor.url_name = 'monitor'
def history(modeladmin, request, queryset):
- resources = OrderedDict()
- names = {}
- for data in queryset:
- resource = data.resource
- try:
- objects, totals, all_dates = resources[resource]
- except KeyError:
- objects, totals, all_dates = OrderedDict(), OrderedDict(), set()
- resources[resource] = (objects, totals, all_dates)
- scale = resource.get_scale()
- # Per monitor
- aggregate = len(resource.monitors) > 1
- for monitor, dataset in data.get_monitor_datasets():
- for date, dataset in resource.aggregation_instance.historic_filter(dataset):
- if dataset is None:
- break
- all_dates.add(date)
- # Per object
- for mdata, usage in resource.aggregation_instance.compute_historic_usage(dataset):
- # objects
- usage = float(usage or 0)/scale
- key = (monitor, mdata.object_id)
- names[key] = mdata.content_object_repr
- try:
- dates = objects[key]
- except KeyError:
- dates = {
- date: usage
- }
- objects[key] = dates
- dates[date] = usage
- if aggregate:
- # Totals
- key = (data)
- try:
- dates = totals[data]
- except KeyError:
- dates = {
- date: usage
- }
- totals[data] = dates
- try:
- dates[date] += usage
- except KeyError:
- dates[date] = usage
- results = []
- from .backends import ServiceMonitor
- import json
- from django.template.defaultfilters import date as date_filter
- for resource, content in resources.items():
- object_result = []
- total_result = []
- objects, totals, all_dates = content
- all_dates = list(sorted(all_dates))
- for key, dates in objects.items():
- name = names[key]
- data = []
- for date in all_dates:
- try:
- data.append(round(dates[date], 2))
- except KeyError:
- data.append(0)
- object_result.append({
- 'name': name,
- 'data': data,
- })
- if len(totals) > 1:
- for rdata, dates in totals.items():
- name = rdata.content_object_repr
- data = []
- for date in all_dates:
- try:
- data.append(round(dates[date], 2))
- except KeyError:
- data.append(0)
- total_result.append({
- 'name': name,
- 'data': data,
- })
- results.append((resource, object_result, total_result, all_dates))
-
-
context = {
- 'resources': results
+ 'ids': ','.join(map(str, queryset.values_list('id', flat=True))),
}
return render(request, 'admin/resources/resourcedata/history.html', context)
history.url_name = 'history'
diff --git a/orchestra/contrib/resources/admin.py b/orchestra/contrib/resources/admin.py
index c0240671..bbcb7240 100644
--- a/orchestra/contrib/resources/admin.py
+++ b/orchestra/contrib/resources/admin.py
@@ -19,6 +19,7 @@ from orchestra.utils import db, sys
from orchestra.utils.functional import cached
from .actions import run_monitor, history
+from .api import history_data
from .filters import ResourceDataListFilter
from .forms import ResourceForm
from .models import Resource, ResourceData, MonitorData
@@ -109,13 +110,13 @@ content_object_link.allow_tags = True
class ResourceDataAdmin(ExtendedModelAdmin):
list_display = (
- 'id', 'resource_link', content_object_link, 'allocated', 'display_used', 'display_unit',
+ 'id', 'resource_link', content_object_link, 'allocated', 'display_used',
'display_updated'
)
list_filter = ('resource',)
fields = (
'resource_link', 'content_type', content_object_link, 'display_updated', 'display_used',
- 'allocated', 'display_unit'
+ 'allocated',
)
search_fields = ('content_object_repr',)
readonly_fields = fields
@@ -137,22 +138,21 @@ class ResourceDataAdmin(ExtendedModelAdmin):
admin_site.admin_view(self.used_monitordata_view),
name='%s_%s_used_monitordata' % (opts.app_label, opts.model_name)
),
+ url('^history_data/$',
+ admin_site.admin_view(history_data),
+ name='%s_%s_history_data' % (opts.app_label, opts.model_name)
+ ),
url('^list-related/(.+)/(.+)/(\d+)/$',
admin_site.admin_view(self.list_related_view),
name='%s_%s_list_related' % (opts.app_label, opts.model_name)
),
] + urls
- def display_unit(self, rdata):
- return rdata.unit
- display_unit.short_description = _("Unit")
- display_unit.admin_order_field = 'resource__unit'
-
def display_used(self, rdata):
if rdata.used is None:
return ''
url = reverse('admin:resources_resourcedata_used_monitordata', args=(rdata.pk,))
- return '%s' % (url, rdata.used)
+ return '%s %s' % (url, rdata.used, rdata.unit)
display_used.short_description = _("Used")
display_used.admin_order_field = 'used'
display_used.allow_tags = True
@@ -194,7 +194,7 @@ class ResourceDataAdmin(ExtendedModelAdmin):
class MonitorDataAdmin(ExtendedModelAdmin):
- list_display = ('id', 'monitor', 'display_created', 'value', content_object_link)
+ list_display = ('id', 'monitor', content_object_link, 'display_created', 'value')
list_filter = ('monitor', ResourceDataListFilter)
add_fields = ('monitor', 'content_type', 'object_id', 'created_at', 'value')
fields = ('monitor', 'content_type', content_object_link, 'display_created', 'value')
diff --git a/orchestra/contrib/resources/aggregations.py b/orchestra/contrib/resources/aggregations.py
index ec23f54e..d4488815 100644
--- a/orchestra/contrib/resources/aggregations.py
+++ b/orchestra/contrib/resources/aggregations.py
@@ -6,25 +6,24 @@ from dateutil.relativedelta import relativedelta
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
+from orchestra.utils.python import AttrDict
+
from orchestra import plugins
class Aggregation(plugins.Plugin, metaclass=plugins.PluginMount):
""" filters and computes dataset usage """
+ aggregated_history = False
+
def filter(self, dataset):
""" Filter the dataset to get the relevant data according to the period """
raise NotImplementedError
- def historic_filter(self, dataset):
- """ Generates (date, dataset) tuples for resource data history reporting """
- raise NotImplementedError
-
def compute_usage(self, dataset):
""" given a dataset computes its usage according to the method (avg, sum, ...) """
raise NotImplementedError
- def compute_historic_usage(self, dataset):
- """ generates [(data, usage),] tuples for resource data history reporting """
+ def aggregate_history(self, dataset):
raise NotImplementedError
@@ -39,42 +38,32 @@ class Last(Aggregation):
dataset = dataset.filter(created_at__lte=date)
return dataset
- def monthly_historic_filter(self, dataset):
- today = timezone.now().date()
- date = datetime.date(
- year=today.year,
- month=today.month,
- day=1,
- )
- while True:
- dataset_copy = copy.copy(dataset)
- dataset_copy = self.filter(dataset_copy, date=date)
- try:
- dataset_copy[0]
- except IndexError:
- yield (date, None)
- yield (date, dataset_copy)
- date -= relativedelta(months=1)
-
- def historic_filter(self, dataset):
- yield (timezone.now().date(), self.filter(dataset))
- yield from self.monthly_historic_filter(dataset)
-
def compute_usage(self, dataset):
values = dataset.values_list('value', flat=True)
if values:
return sum(values)
return None
- def compute_historic_usage(self, dataset):
- dataset = dataset.only('object_id', 'value', 'content_object_repr')
- return [(mdata, mdata.value) for mdata in dataset]
+ def aggregate_history(self, dataset):
+ prev_object_id = None
+ for mdata in dataset.order_by('object_id', 'created_at'):
+ object_id = mdata.object_id
+ if object_id != prev_object_id:
+ if prev_object_id is not None:
+ yield (mdata.content_object_repr, datas)
+ datas = [mdata]
+ else:
+ datas.append(mdata)
+ prev_object_id = object_id
+ if prev_object_id is not None:
+ yield (mdata.content_object_repr, datas)
class MonthlySum(Last):
""" Monthly sum the values of all monitors """
name = 'monthly-sum'
verbose_name = _("Monthly Sum")
+ aggregated_history = True
def filter(self, dataset, date=None):
if date is None:
@@ -84,25 +73,45 @@ class MonthlySum(Last):
created_at__month=date.month,
)
- def historic_filter(self, dataset):
- yield from self.monthly_historic_filter(dataset)
-
- def compute_historic_usage(self, dataset):
- objects = {}
- mdatas = {}
- for mdata in dataset.only('object_id', 'value', 'content_object_repr'):
- mdatas[mdata.object_id] = mdata
- try:
- objects[mdata.object_id] += mdata.value
- except KeyError:
- objects[mdata.object_id] = mdata.value
- return [(mdatas[object_id], value) for object_id, value in objects.items()]
+ def aggregate_history(self, dataset):
+ make_data = lambda mdata, current: AttrDict(
+ date=datetime.date(
+ year=mdata.created_at.year,
+ month=mdata.created_at.month,
+ day=1
+ ),
+ value=current,
+ content_object_repr=mdata.content_object_repr
+ )
+
+ prev_month = None
+ prev_object_id = None
+ datas = []
+ for mdata in dataset.order_by('object_id', 'created_at'):
+ object_id = mdata.object_id
+ if object_id != prev_object_id:
+ if prev_object_id is not None:
+ yield (mdata.content_object_repr, datas)
+ datas = []
+ month = mdata.created_at.month
+ if object_id != prev_object_id or month != prev_month:
+ if prev_month is not None:
+ datas.append(make_data(mdata, current))
+ current = mdata.value
+ else:
+ current += mdata.value
+ prev_month = month
+ prev_object_id = object_id
+ if prev_object_id is not None:
+ datas.append(make_data(mdata, current))
+ yield (mdata.content_object_repr, datas)
class MonthlyAvg(MonthlySum):
""" sum of the monthly averages of each monitor """
name = 'monthly-avg'
verbose_name = _("Monthly AVG")
+ aggregated_history = False
def get_epoch(self, date=None):
if date is None:
@@ -143,8 +152,8 @@ class MonthlyAvg(MonthlySum):
return result
return None
- def compute_historic_usage(self, dataset):
- return self.compute_usage(dataset, historic=True)
+ def aggregate_history(self, dataset):
+ yield from super(MonthlySum, self).aggregate_history(dataset)
class Last10DaysAvg(MonthlyAvg):
@@ -164,7 +173,3 @@ class Last10DaysAvg(MonthlyAvg):
if date is not None:
dataset = dataset.filter(created_at__lte=date)
return dataset
-
- def historic_filter(self, dataset):
- yield (timezone.now().date(), self.filter(dataset))
- yield from super(Last10DaysAvg, self).historic_filter(dataset)
diff --git a/orchestra/contrib/resources/api.py b/orchestra/contrib/resources/api.py
new file mode 100644
index 00000000..0efb4815
--- /dev/null
+++ b/orchestra/contrib/resources/api.py
@@ -0,0 +1,19 @@
+import json
+from urllib.parse import parse_qs
+
+from django.http import HttpResponse
+
+from .helpers import get_history_data
+from .models import ResourceData
+
+
+def history_data(request):
+ ids = map(int, parse_qs(request.META['QUERY_STRING'])['ids'][0].split(','))
+ queryset = ResourceData.objects.filter(id__in=ids)
+ history = get_history_data(queryset)
+ def default(obj):
+ if isinstance(obj, set):
+ return list(obj)
+ return obj
+ response = json.dumps(history, default=default, indent=4)
+ return HttpResponse(response, content_type="application/json")
diff --git a/orchestra/contrib/resources/backends.py b/orchestra/contrib/resources/backends.py
index 8e75d017..9430f025 100644
--- a/orchestra/contrib/resources/backends.py
+++ b/orchestra/contrib/resources/backends.py
@@ -71,7 +71,7 @@ class ServiceMonitor(ServiceBackend):
raise ValueError("%s expected ' ' got '%s'" % (cls_name, line))
if isinstance(value, bytes):
value = value.decode('ascii')
- content_object = get_object_for_this_type(pk=object_id)
+ content_object = ct.get_object_for_this_type(pk=object_id)
MonitorData.objects.create(
monitor=name, object_id=object_id, content_type=ct, value=value,
created_at=self.current_date, content_object_repr=str(content_object),
diff --git a/orchestra/contrib/resources/helpers.py b/orchestra/contrib/resources/helpers.py
new file mode 100644
index 00000000..042f4104
--- /dev/null
+++ b/orchestra/contrib/resources/helpers.py
@@ -0,0 +1,81 @@
+from django.template.defaultfilters import date as date_filter
+
+
+def get_history_data(queryset):
+ resources = {}
+ needs_aggregation = False
+ for rdata in queryset:
+ resource = rdata.resource
+ try:
+ (options, aggregation) = resources[resource]
+ except KeyError:
+ aggregation = resource.aggregation_instance
+ options = {
+ 'aggregation': str(aggregation.verbose_name),
+ 'aggregated_history': aggregation.aggregated_history,
+ 'content_type': rdata.content_type.model,
+ 'content_object': rdata.content_object_repr,
+ 'unit': resource.unit,
+ 'scale': resource.get_scale(),
+ 'verbose_name': str(resource.verbose_name),
+ 'dates': set(),
+ 'objects': [],
+ }
+ resources[resource] = (options, aggregation)
+
+ monitors = []
+ scale = options['scale']
+ all_dates = options['dates']
+ for monitor_name, dataset in rdata.get_monitor_datasets():
+ datasets = {}
+ for content_object, datas in aggregation.aggregate_history(dataset):
+ if aggregation.aggregated_history:
+ needs_aggregation = True
+ serie = {}
+ for data in datas:
+ date = date_filter(data.date)
+ value = round(float(data.value or 0)/scale, 2)
+ all_dates.add(date)
+ serie[date] = value
+ else:
+ serie = []
+ for data in datas:
+ date = data.created_at.timestamp()
+ date = int(str(date).split('.')[0] + '000')
+ value = round(float(data.value or 0)/scale, 2)
+ serie.append(
+ (date, value)
+ )
+ datasets[content_object] = serie
+ monitors.append({
+ 'name': monitor_name,
+ 'datasets': datasets,
+ })
+ options['objects'].append({
+ 'object_name': rdata.content_object_repr,
+ 'current': round(float(rdata.used), 3),
+ 'allocated': float(rdata.allocated) if rdata.allocated is not None else None,
+ 'updated_at': rdata.updated_at.isoformat(),
+ 'monitors': monitors,
+ })
+ if needs_aggregation:
+ result = []
+ for options, aggregation in resources.values():
+ if aggregation.aggregated_history:
+ all_dates = options['dates']
+ for obj in options['objects']:
+ for monitor in obj['monitors']:
+ series = []
+ for content_object, dataset in monitor['datasets'].items():
+ data = []
+ for date in all_dates:
+ data.append(dataset.get(date, 0.0))
+ series.append({
+ 'name': content_object,
+ 'data': data,
+ })
+ monitor['datasets'] = series
+ result.append(options)
+ else:
+ result = [resource[0] for resource in resources.values()]
+ return result
diff --git a/orchestra/contrib/resources/migrations/0006_auto_20150723_1249.py b/orchestra/contrib/resources/migrations/0006_auto_20150723_1249.py
new file mode 100644
index 00000000..ff1771f4
--- /dev/null
+++ b/orchestra/contrib/resources/migrations/0006_auto_20150723_1249.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0005_auto_20150723_0940'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='monitordata',
+ name='created_at',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='created', db_index=True),
+ ),
+ migrations.AlterField(
+ model_name='monitordata',
+ name='monitor',
+ field=models.CharField(max_length=256, choices=[('Apache2Traffic', '[M] Apache 2 Traffic'), ('DovecotMaildirDisk', '[M] Dovecot Maildir size'), ('Exim4Traffic', '[M] Exim4 traffic'), ('MailmanSubscribers', '[M] Mailman subscribers'), ('MailmanTraffic', '[M] Mailman traffic'), ('MysqlDisk', '[M] MySQL disk'), ('OpenVZTraffic', '[M] OpenVZTraffic'), ('PostfixMailscannerTraffic', '[M] Postfix-Mailscanner traffic'), ('UNIXUserDisk', '[M] UNIX user disk'), ('VsFTPdTraffic', '[M] VsFTPd traffic')], verbose_name='monitor', db_index=True),
+ ),
+ migrations.AlterField(
+ model_name='monitordata',
+ name='object_id',
+ field=models.PositiveIntegerField(verbose_name='object id', db_index=True),
+ ),
+ ]
diff --git a/orchestra/contrib/resources/migrations/0007_auto_20150723_1251.py b/orchestra/contrib/resources/migrations/0007_auto_20150723_1251.py
new file mode 100644
index 00000000..6b082111
--- /dev/null
+++ b/orchestra/contrib/resources/migrations/0007_auto_20150723_1251.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('resources', '0006_auto_20150723_1249'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='resourcedata',
+ name='object_id',
+ field=models.PositiveIntegerField(verbose_name='object id', db_index=True),
+ ),
+ ]
diff --git a/orchestra/contrib/resources/models.py b/orchestra/contrib/resources/models.py
index 8a4a4adb..b49497c7 100644
--- a/orchestra/contrib/resources/models.py
+++ b/orchestra/contrib/resources/models.py
@@ -167,8 +167,8 @@ class Resource(models.Model):
class ResourceData(models.Model):
""" Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
- content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
- object_id = models.PositiveIntegerField(_("object id"))
+ content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), db_index=True)
+ object_id = models.PositiveIntegerField(_("object id"), db_index=True)
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
editable=False)
updated_at = models.DateTimeField(_("updated"), null=True, editable=False)
@@ -262,11 +262,11 @@ class MonitorDataQuerySet(models.QuerySet):
class MonitorData(models.Model):
""" Stores monitored data """
- monitor = models.CharField(_("monitor"), max_length=256,
+ monitor = models.CharField(_("monitor"), max_length=256, db_index=True,
choices=ServiceMonitor.get_choices())
- content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
- object_id = models.PositiveIntegerField(_("object id"))
- created_at = models.DateTimeField(_("created"), default=timezone.now)
+ content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), db_index=True)
+ object_id = models.PositiveIntegerField(_("object id"), db_index=True)
+ created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
content_object_repr = models.CharField(_("content object representation"), max_length=256,
editable=False)
diff --git a/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html b/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html
index 35b5b26d..c6b0a80d 100644
--- a/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html
+++ b/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html
@@ -5,32 +5,63 @@
Resource history
-
+
- {% for resource, __, totals, __ in resources %}
-
-
-
{{ resource.content_type|capfirst }} {{ resource.verbose_name.lower }}
- {% if totals %}
-
-
- {% endif %}
-
+
+ Crunching data ...
-
- {% endfor %}