Added resource history support

This commit is contained in:
Marc Aymerich 2015-07-16 13:07:15 +00:00
parent 7401db390f
commit 48a19d8be9
8 changed files with 219 additions and 38 deletions

View file

@ -423,3 +423,5 @@ Colaesce('total', 'computed_total')
Case Case
# case on payment transaction state ? case when trans.amount > # case on payment transaction state ? case when trans.amount >
# Yield multiple values on historic filter on all aggregators

View file

@ -1,3 +1,5 @@
from django.utils.translation import ungettext, ugettext, ugettext_lazy as _
from orchestra.contrib.settings import Setting from orchestra.contrib.settings import Setting
from .. import payments from .. import payments

View file

@ -1,5 +1,5 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect, render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
@ -38,3 +38,29 @@ def run_monitor(modeladmin, request, queryset):
if referer: if referer:
return redirect(referer) return redirect(referer)
run_monitor.url_name = 'monitor' run_monitor.url_name = 'monitor'
def history(modeladmin, request, queryset):
resources = {}
for data in queryset:
resource = data.resource
total = 0
totals = {}
for dataset in data.get_monitor_datasets():
for date, dataset in resource.aggregation_instance.historic_filter(dataset):
usage = resource.aggregation_instance.compute_usage(dataset)
if usage is not None:
try:
totals[date] += usage
except KeyError:
totals[date] = usage
scale = resource.get_scale()
for date, total in totals.items():
totals[date] = float(total)/scale
totals = list(sorted(totals.items()))
resources[data] = totals
context = {
'resources': resources,
}
return render(request, 'admin/resources/resourcedata/report.html', context)
history.url_name = 'history'

View file

@ -17,7 +17,7 @@ from orchestra.core import services
from orchestra.utils import db, sys from orchestra.utils import db, sys
from orchestra.utils.functional import cached from orchestra.utils.functional import cached
from .actions import run_monitor from .actions import run_monitor, history
from .filters import ResourceDataListFilter from .filters import ResourceDataListFilter
from .forms import ResourceForm from .forms import ResourceForm
from .models import Resource, ResourceData, MonitorData from .models import Resource, ResourceData, MonitorData
@ -109,7 +109,7 @@ class ResourceDataAdmin(ExtendedModelAdmin):
) )
search_fields = ('object_id',) search_fields = ('object_id',)
readonly_fields = fields readonly_fields = fields
actions = (run_monitor,) actions = (run_monitor, history)
change_view_actions = actions change_view_actions = actions
ordering = ('-updated_at',) ordering = ('-updated_at',)
list_select_related = ('resource__content_type',) list_select_related = ('resource__content_type',)
@ -117,7 +117,7 @@ class ResourceDataAdmin(ExtendedModelAdmin):
resource_link = admin_link('resource') resource_link = admin_link('resource')
content_object_link = admin_link('content_object') content_object_link = admin_link('content_object')
content_object_link.admin_order_field = None content_object_link.admin_order_field = 'object_id'
display_updated = admin_date('updated_at', short_description=_("Updated")) display_updated = admin_date('updated_at', short_description=_("Updated"))
def get_urls(self): def get_urls(self):
@ -170,8 +170,10 @@ class MonitorDataAdmin(ExtendedModelAdmin):
resource_data = query_string.get('resource_data') resource_data = query_string.get('resource_data')
if resource_data: if resource_data:
data = ResourceData.objects.get(pk=int(resource_data[0])) data = ResourceData.objects.get(pk=int(resource_data[0]))
resource = data.resource
ids = [] ids = []
for dataset in data.get_monitor_datasets(): for dataset in data.get_monitor_datasets():
dataset = resource.aggregation_instance.filter(dataset)
if isinstance(dataset, MonitorData): if isinstance(dataset, MonitorData):
ids.append(dataset.id) ids.append(dataset.id)
else: else:
@ -242,7 +244,7 @@ def resource_inline_factory(resources):
fields = ( fields = (
'verbose_name', 'display_used', 'display_updated', 'allocated', 'unit', 'verbose_name', 'display_used', 'display_updated', 'allocated', 'unit',
) )
readonly_fields = ('display_used', 'display_updated') readonly_fields = ('display_used', 'display_updated',)
class Media: class Media:
css = { css = {
@ -253,11 +255,15 @@ def resource_inline_factory(resources):
def display_used(self, data): def display_used(self, data):
update_link = '' update_link = ''
history_link = ''
if data.pk: if data.pk:
url = reverse('admin:resources_resourcedata_monitor', args=(data.pk,)) update_url = reverse('admin:resources_resourcedata_monitor', args=(data.pk,))
update_link = '<a href="%s"><strong>%s</strong></a>' % (url, ugettext("Update")) update_link = '<a href="%s"><strong>%s</strong></a>' % (update_url, _("Update"))
history_url = reverse('admin:resources_resourcedata_history', args=(data.pk,))
popup = 'onclick="return showAddAnotherPopup(this);"'
history_link = '<a href="%s" %s>%s</a>' % (history_url, popup, _("History"))
if data.used is not None: if data.used is not None:
return '%s %s %s' % (data.used, data.resource.unit, update_link) return ' '.join(map(str, (data.used, data.resource.unit, update_link, history_link)))
return _("Unknonw %s") % update_link return _("Unknonw %s") % update_link
display_used.short_description = _("Used") display_used.short_description = _("Used")
display_used.allow_tags = True display_used.allow_tags = True

View file

@ -1,6 +1,8 @@
import copy
import datetime import datetime
import decimal import decimal
from dateutil.relativedelta import relativedelta
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -13,6 +15,10 @@ class Aggregation(plugins.Plugin, metaclass=plugins.PluginMount):
""" Filter the dataset to get the relevant data according to the period """ """ Filter the dataset to get the relevant data according to the period """
raise NotImplementedError raise NotImplementedError
def historic_filter(self, dataset):
""" Generates (date, dataset) tuples for resource data history reporting """
raise NotImplementedError
def compute_usage(self, dataset): def compute_usage(self, dataset):
""" given a dataset computes its usage according to the method (avg, sum, ...) """ """ given a dataset computes its usage according to the method (avg, sum, ...) """
raise NotImplementedError raise NotImplementedError
@ -23,11 +29,30 @@ class Last(Aggregation):
name = 'last' name = 'last'
verbose_name = _("Last value") verbose_name = _("Last value")
def filter(self, dataset): def filter(self, dataset, date=None):
dataset = dataset.order_by('object_id', '-id').distinct('monitor')
if date is not None:
dataset = dataset.filter(created_at__lte=date)
return dataset
def historic_filter(self, dataset):
yield (timezone.now(), self.filter(dataset))
now = timezone.now()
date = datetime.datetime(
year=now.year,
month=now.month,
day=1,
tzinfo=timezone.utc,
)
while True:
dataset_copy = copy.copy(dataset)
dataset_copy = self.filter(dataset_copy, date=date)
try: try:
return dataset.order_by('object_id', '-id').distinct('monitor') dataset_copy[0]
except dataset.model.DoesNotExist: except IndexError:
return dataset.none() raise StopIteration
yield (date, dataset_copy)
date -= relativedelta(months=1)
def compute_usage(self, dataset): def compute_usage(self, dataset):
values = dataset.values_list('value', flat=True) values = dataset.values_list('value', flat=True)
@ -41,30 +66,50 @@ class MonthlySum(Last):
name = 'monthly-sum' name = 'monthly-sum'
verbose_name = _("Monthly Sum") verbose_name = _("Monthly Sum")
def filter(self, dataset): def filter(self, dataset, date=None):
today = timezone.now() if date is None:
date = timezone.now()
return dataset.filter( return dataset.filter(
created_at__year=today.year, created_at__year=date.year,
created_at__month=today.month created_at__month=date.month,
) )
def historic_filter(self, dataset):
now = timezone.now()
date = datetime.datetime(
year=now.year,
month=now.month,
day=1,
tzinfo=timezone.utc,
)
while True:
dataset_copy = copy.copy(dataset)
dataset_copy = self.filter(dataset_copy, date=date)
try:
dataset_copy[0]
except IndexError:
raise StopIteration
yield (date, dataset_copy)
date -= relativedelta(months=1)
class MonthlyAvg(MonthlySum): class MonthlyAvg(MonthlySum):
""" sum of the monthly averages of each monitor """ """ sum of the monthly averages of each monitor """
name = 'monthly-avg' name = 'monthly-avg'
verbose_name = _("Monthly AVG") verbose_name = _("Monthly AVG")
def filter(self, dataset): def filter(self, dataset, date=None):
qs = super(MonthlyAvg, self).filter(dataset) qs = super(MonthlyAvg, self).filter(dataset, date=date)
return qs.order_by('created_at') return qs.order_by('created_at')
def get_epoch(self): def get_epoch(self, date=None):
today = timezone.now() if date is None:
return datetime( date = timezone.now()
year=today.year, return datetime.datetime(
month=today.month, year=date.year,
month=date.month,
day=1, day=1,
tzinfo=timezone.utc tzinfo=timezone.utc,
) )
def compute_usage(self, dataset): def compute_usage(self, dataset):
@ -75,7 +120,7 @@ class MonthlyAvg(MonthlySum):
last = dataset[-1] last = dataset[-1]
except IndexError: except IndexError:
continue continue
epoch = self.get_epoch() epoch = self.get_epoch(date=last.created_at)
total = (last.created_at-epoch).total_seconds() total = (last.created_at-epoch).total_seconds()
ini = epoch ini = epoch
for data in dataset: for data in dataset:
@ -94,10 +139,18 @@ class Last10DaysAvg(MonthlyAvg):
verbose_name = _("Last 10 days AVG") verbose_name = _("Last 10 days AVG")
days = 10 days = 10
def get_epoch(self): def get_epoch(self, date=None):
today = timezone.now() if date is None:
return today - datetime.timedelta(days=self.days) date = timezone.now()
return date - datetime.timedelta(days=self.days)
def filter(self, dataset): def filter(self, dataset, date=None):
epoch = self.get_epoch() epoch = self.get_epoch(date=date)
return dataset.filter(created_at__gt=epoch).order_by('created_at') dataset = dataset.filter(created_at__gt=epoch).order_by('created_at')
if date is not None:
dataset = dataset.filter(created_at__lte=date)
return dataset
def historic_filter(self, dataset):
yield (timezone.now(), self.filter(dataset))
yield from super(Last10DaysAvg, self).historic_filter(dataset)

View file

@ -209,6 +209,7 @@ class ResourceData(models.Model):
total = 0 total = 0
has_result = False has_result = False
for dataset in self.get_monitor_datasets(): for dataset in self.get_monitor_datasets():
dataset = resource.aggregation_instance.filter(dataset)
usage = resource.aggregation_instance.compute_usage(dataset) usage = resource.aggregation_instance.compute_usage(dataset)
if usage is not None: if usage is not None:
has_result = True has_result = True
@ -237,7 +238,7 @@ class ResourceData(models.Model):
dataset = MonitorData.objects.filter( dataset = MonitorData.objects.filter(
monitor=monitor, monitor=monitor,
content_type=self.content_type_id, content_type=self.content_type_id,
object_id=self.object_id object_id=self.object_id,
) )
else: else:
fields = '__'.join(path) fields = '__'.join(path)
@ -248,11 +249,9 @@ class ResourceData(models.Model):
dataset = MonitorData.objects.filter( dataset = MonitorData.objects.filter(
monitor=monitor, monitor=monitor,
content_type=ct, content_type=ct,
object_id__in=pks object_id__in=pks,
)
datasets.append(
resource.aggregation_instance.filter(dataset)
) )
datasets.append(dataset)
return datasets return datasets

View file

@ -0,0 +1,82 @@
{% load i18n utils %}
<html>
<head>
<title>Transaction Report</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script src="/static/orchestra/js/Chart.min.js"></script>
<style type="text/css">
@page {
size: 11.69in 8.27in;
}
h3 {
font-family: sans;
font-size: 13px;
}
table {
max-width: 10in;
font-family: sans;
font-size: 10px;
}
table tr:nth-child(even) {
background-color: #eee;
}
table tr:nth-child(odd) {
background-color: #fff;
}
table th {
color: white;
background-color: grey;
}
.item.column-created, .item.column-updated {
text-align: center;
}
.item.column-amount {
text-align: right;
}
.footnote {
font-family: sans;
font-size: 10px;
}
</style>
</head>
<body>
{% for data, totals in resources.items %}
<h3>{{ data.resource.get_verbose_name }} {{ data.content_object }} - {{ data.resource.aggregation_instance.verbose_name }}</h3>
<canvas id="chart-{{ data.id }}" width="600" height="400"></canvas>
<script>
// bar chart data
var barData = {
labels : [{% for date, total in totals %}"{{ date|date }}"{% if not forloop.last %},{% endif %}{% endfor %}],
datasets : [
{
fillColor : "#48A497",
strokeColor : "#48A4D1",
data : [{% for date, total in totals %}{{ total|floatformat:3 }}{% if not forloop.last %},{% endif %}{% endfor %}]
}
]
}
var income = document.getElementById("chart-{{ data.id }}").getContext("2d");
var options = {
scaleLabel: "<%=value%> {{ data.resource.unit }}",
}
new Chart(income).Bar(barData, options);
</script>
<table id="summary">
<tr class="header">
<th class="title column-name">{% trans "Date" %}</th>
<th class="title column-active">{% trans "Used" %}</th>
</tr>
{% for date, total in totals %}
<tr>
<td class="item column-name">{{ date|date }}</td>
<td class="item column-amount">{{ total|floatformat:3 }} {{ data.resource.unit }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
</body>
</html>

File diff suppressed because one or more lines are too long