Added resource history support
This commit is contained in:
parent
7401db390f
commit
48a19d8be9
2
TODO.md
2
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
@ -265,7 +271,7 @@ def resource_inline_factory(resources):
|
||||||
def has_add_permission(self, *args, **kwargs):
|
def has_add_permission(self, *args, **kwargs):
|
||||||
""" Hidde add another """
|
""" Hidde add another """
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return ResourceInline
|
return ResourceInline
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
try:
|
dataset = dataset.order_by('object_id', '-id').distinct('monitor')
|
||||||
return dataset.order_by('object_id', '-id').distinct('monitor')
|
if date is not None:
|
||||||
except dataset.model.DoesNotExist:
|
dataset = dataset.filter(created_at__lte=date)
|
||||||
return dataset.none()
|
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:
|
||||||
|
dataset_copy[0]
|
||||||
|
except IndexError:
|
||||||
|
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,12 +66,31 @@ 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):
|
||||||
|
@ -54,17 +98,18 @@ class MonthlyAvg(MonthlySum):
|
||||||
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)
|
||||||
|
|
|
@ -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(
|
datasets.append(dataset)
|
||||||
resource.aggregation_instance.filter(dataset)
|
|
||||||
)
|
|
||||||
return datasets
|
return datasets
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
11
orchestra/static/orchestra/js/Chart.min.js
vendored
Normal file
11
orchestra/static/orchestra/js/Chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue