Skip to content

Commit 8a1ad2f

Browse files
authored
Merge pull request #85 from stackhpc/upstream/master-2026-03-30
Synchronise master with upstream
2 parents f5823b0 + 5166b79 commit 8a1ad2f

25 files changed

Lines changed: 1107 additions & 110 deletions

File tree

cloudkittydashboard/dashboards/admin/summary/tables.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
from django.conf import settings
1516
from django.urls import reverse
1617
from django.utils.translation import gettext_lazy as _
1718

1819
from horizon import tables
1920

21+
from cloudkittydashboard.utils import formatTitle
22+
2023

2124
def get_details_link(datum):
2225
if datum.tenant_id:
@@ -37,12 +40,36 @@ class Meta(object):
3740

3841

3942
class TenantSummaryTable(tables.DataTable):
40-
res_type = tables.Column('type', verbose_name=_("Resource Type"))
43+
groupby_list = getattr(settings,
44+
'OPENSTACK_CLOUDKITTY_GROUPBY_LIST',
45+
['type'])
46+
47+
# Dynamically create columns based on groupby_list
48+
for field in groupby_list:
49+
locals()[field] = tables.Column(
50+
field, verbose_name=_(formatTitle(field)))
51+
4152
rate = tables.Column('rate', verbose_name=_("Rate"))
4253

54+
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
55+
super().__init__(request, data, needs_form_wrapper, **kwargs)
56+
57+
# Hide columns based on checkbox selection
58+
for field in self.groupby_list:
59+
if request.GET.get(field) != 'true':
60+
self.columns[field].classes = ['hidden']
61+
4362
class Meta(object):
4463
name = "tenant_summary"
4564
verbose_name = _("Project Summary")
4665

4766
def get_object_id(self, datum):
48-
return datum.get('type')
67+
# Prevents the table from displaying the same ID for different rows
68+
id_parts = []
69+
for field in self.groupby_list:
70+
if field in datum and datum[field]:
71+
id_parts.append(str(datum[field]))
72+
73+
if id_parts:
74+
return '_'.join(id_parts)
75+
return _('No IDs found')

cloudkittydashboard/dashboards/admin/summary/templates/rating_summary/details.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
{% trans "Project ID:" %} {{ project_id }}
1212

13+
{{ groupby_list|json_script:"groupby_list_config" }}
14+
{% include "project/rating/groupby.html" %}
1315
{{ table.render }}
1416

1517
{{ modules }}

cloudkittydashboard/dashboards/admin/summary/views.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from cloudkittydashboard.dashboards.admin.summary import tables as sum_tables
2323
from cloudkittydashboard import utils
2424

25+
from cloudkittydashboard import forms
26+
2527
rate_prefix = getattr(settings,
2628
'OPENSTACK_CLOUDKITTY_RATE_PREFIX', None)
2729
rate_postfix = getattr(settings,
@@ -35,8 +37,7 @@ class IndexView(tables.DataTableView):
3537
def get_data(self):
3638
summary = api.cloudkittyclient(
3739
self.request, version='2').summary.get_summary(
38-
groupby=['project_id'],
39-
response_format='object')
40+
groupby=['project_id'], response_format='object')
4041

4142
tenants, unused = api_keystone.tenant_list(self.request)
4243
tenants = {tenant.id: tenant.name for tenant in tenants}
@@ -47,6 +48,7 @@ def get_data(self):
4748
'project_id': 'ALL',
4849
'rate': total,
4950
})
51+
5052
data = api.identify(data, key='project_id')
5153
for tenant in data:
5254
tenant['tenant_id'] = tenant.get('project_id')
@@ -60,27 +62,42 @@ def get_data(self):
6062
class TenantDetailsView(tables.DataTableView):
6163
template_name = 'admin/rating_summary/details.html'
6264
table_class = sum_tables.TenantSummaryTable
63-
page_title = _("Script details: {{ table.project_id }}")
65+
66+
def get_context_data(self, **kwargs):
67+
context = super().get_context_data(**kwargs)
68+
context['groupby_list'] = getattr(settings,
69+
'OPENSTACK_CLOUDKITTY_GROUPBY_LIST',
70+
['type'])
71+
return context
6472

6573
def get_data(self):
6674
tenant_id = self.kwargs['project_id']
75+
form = forms.CheckBoxForm(self.request.GET)
76+
groupby = form.get_selected_fields()
6777

6878
if tenant_id == 'ALL':
6979
summary = api.cloudkittyclient(
70-
self.request, version='2').summary.get_summary(
71-
groupby=['type'], response_format='object')
80+
self.request, version='2'
81+
).summary.get_summary(groupby=groupby, response_format='object')
7282
else:
7383
summary = api.cloudkittyclient(
74-
self.request, version='2').summary.get_summary(
75-
filters={'project_id': tenant_id},
76-
groupby=['type'], response_format='object')
84+
self.request, version='2'
85+
).summary.get_summary(
86+
filters={'project_id': tenant_id},
87+
groupby=groupby,
88+
response_format='object',
89+
)
7790

7891
data = summary.get('results')
7992
total = sum([r.get('rate') for r in data])
80-
data.append({'type': 'TOTAL', 'rate': total})
8193

82-
for item in data:
83-
item['rate'] = utils.formatRate(item['rate'],
84-
rate_prefix, rate_postfix)
94+
if not groupby:
95+
data = [{'type': 'TOTAL', 'rate': total}]
96+
97+
else:
98+
data.append({'type': 'TOTAL', 'rate': total})
99+
for item in data:
100+
item['rate'] = utils.formatRate(
101+
item['rate'], rate_prefix, rate_postfix)
85102

86103
return data

cloudkittydashboard/dashboards/project/rating/tables.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,47 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14+
from django.conf import settings
1415
from django.utils.translation import gettext_lazy as _
1516

1617
from horizon import tables
1718

19+
from cloudkittydashboard.utils import formatTitle
20+
1821

1922
class SummaryTable(tables.DataTable):
2023
"""This table formats a summary for the given tenant."""
2124

22-
res_type = tables.Column('type', verbose_name=_('Metric Type'))
25+
groupby_list = getattr(settings,
26+
'OPENSTACK_CLOUDKITTY_GROUPBY_LIST',
27+
['type'])
28+
29+
# Dynamically create columns based on groupby_list
30+
for field in groupby_list:
31+
locals()[field] = tables.Column(
32+
field, verbose_name=_(formatTitle(field)))
33+
2334
rate = tables.Column('rate', verbose_name=_('Rate'))
2435

36+
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
37+
super().__init__(request, data, needs_form_wrapper, **kwargs)
38+
39+
# Hide columns based on checkbox selection
40+
for field in self.groupby_list:
41+
if request.GET.get(field) != 'true':
42+
self.columns[field].classes = ['hidden']
43+
2544
class Meta(object):
2645
name = "summary"
2746
verbose_name = _("Summary")
2847

2948
def get_object_id(self, datum):
30-
return datum.get('type')
49+
# prevents the table from displaying the same ID for different rows
50+
id_parts = []
51+
for field in self.groupby_list:
52+
if field in datum and datum[field]:
53+
id_parts.append(str(datum[field]))
54+
55+
if id_parts:
56+
return '_'.join(id_parts)
57+
return _('No IDs found')
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% load i18n %}
2+
{% load static %}
3+
4+
{{ groupby_list|json_script:"groupby_list_config" }}
5+
6+
<!-- For select/unselect button translations -->
7+
<script id="groupby_translations" type="application/json">
8+
{
9+
"select_all": "{% trans 'Select All' %}",
10+
"unselect_all": "{% trans 'Unselect All' %}"
11+
}
12+
</script>
13+
<link rel="stylesheet" href="{% static 'cloudkitty/css/grouping.css' %}">
14+
<script src="{% static 'cloudkitty/js/grouping.js' %}" type="text/javascript" charset="utf-8"></script>
15+
16+
<form method="GET" action="?" id="groupby_checkbox" class="groupby-form">
17+
<h4>{% trans "Group by:" %}</h4>
18+
<div id="checkboxes"></div>
19+
<button class="btn btn-primary" id="toggleAll" type="button">
20+
{% trans "Select All" %}
21+
</button>
22+
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
23+
</form>

cloudkittydashboard/dashboards/project/rating/templates/rating/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
{% block title %}{% trans "Rating Summary" %}{% endblock %}
44

55
{% block page_header %}
6-
{% include "horizon/common/_page_header.html" with title=_("Rating Summary") %}
6+
{% include "horizon/common/_page_header.html" with title=_("Rating Summary") %}
77
{% endblock page_header %}
88

99
{% block main %}
10-
10+
{{ groupby_list|json_script:"groupby_list_config" }}
11+
{% include "project/rating/groupby.html" %}
1112
{{ table.render }}
1213

1314
{{ modules }}

cloudkittydashboard/dashboards/project/rating/views.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from horizon import exceptions
2020
from horizon import tables
2121

22+
from cloudkittydashboard import forms
23+
2224
from cloudkittydashboard.api import cloudkitty as api
2325
from cloudkittydashboard.dashboards.project.rating \
2426
import tables as rating_tables
@@ -34,19 +36,31 @@ class IndexView(tables.DataTableView):
3436
table_class = rating_tables.SummaryTable
3537
template_name = 'project/rating/index.html'
3638

39+
def get_context_data(self, **kwargs):
40+
context = super().get_context_data(**kwargs)
41+
context['groupby_list'] = getattr(settings,
42+
'OPENSTACK_CLOUDKITTY_GROUPBY_LIST',
43+
['type'])
44+
return context
45+
3746
def get_data(self):
47+
form = forms.CheckBoxForm(self.request.GET)
48+
groupby = form.get_selected_fields()
3849
summary = api.cloudkittyclient(
3950
self.request, version='2').summary.get_summary(
4051
tenant_id=self.request.user.tenant_id,
41-
groupby=['type'], response_format='object')
52+
groupby=groupby, response_format='object')
53+
data = summary.get("results")
54+
total = sum([r.get("rate") for r in data])
4255

43-
data = summary.get('results')
44-
total = sum([r.get('rate') for r in data])
56+
if not groupby: # No checkboxes are selected, display total rate only
57+
data = [{"type": "TOTAL", "rate": total}]
4558

46-
data.append({'type': 'TOTAL', 'rate': total})
47-
for item in data:
48-
item['rate'] = utils.formatRate(item['rate'],
49-
rate_prefix, rate_postfix)
59+
else: # Some checkboxes are selected - use groupby
60+
data.append({'type': 'TOTAL', 'rate': total})
61+
for item in data:
62+
item['rate'] = utils.formatRate(item['rate'],
63+
rate_prefix, rate_postfix)
5064
return data
5165

5266

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% load i18n %}
2+
3+
{% block trimmed %}
4+
<div class="input-group date">
5+
{{ datepicker_input }}
6+
<label class="sr-only">{{ datepicker_label }}</label>
7+
<span class="input-group-addon datepicker-addon">
8+
<span class="fa fa-calendar"></span>
9+
</span>
10+
</div>
11+
<script>
12+
$(function () {
13+
$('.datepicker input').datepicker({
14+
todayBtn: "linked", // Enables the today button which quickly links back to today
15+
endDate: '0d', // Prevents the user from picking a date in the future
16+
});
17+
});
18+
</script>
19+
{% endblock %}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{% load i18n %}
2+
{%load static %}
3+
4+
<link rel="stylesheet" href="{% static 'cloudkitty/css/datepicker.css' %}">
5+
<script src="{% static 'cloudkitty/js/datepicker.js' %}" type="text/javascript" charset="utf-8"></script>
6+
7+
<form action="?" method="get" id="{{ datepicker_id }}" class="form-inline">
8+
<h4>{% trans "Select a period of time to view data in:" %}
9+
<span class="small help-block">{% trans "The date should be in YYYY-MM-DD format." %}</span>
10+
</h4>
11+
<div class="datepicker form-group">
12+
{% with datepicker_input=form.start datepicker_label="From" %}
13+
{% include 'project/reporting/_datepicker_reporting.html' %}
14+
{% endwith %}
15+
</div>
16+
<span class="datepicker-delimiter">
17+
<span class="datepicker-delimiter-text">{% trans 'to' %}</span>
18+
</span>
19+
<div class="datepicker form-group">
20+
{% with datepicker_input=form.end datepicker_label="To" %}
21+
{% include 'project/reporting/_datepicker_reporting.html' %}
22+
{% endwith %}
23+
</div>
24+
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
25+
</form>
26+
27+
<div class="controls-container">
28+
<div class="dropdown default-ranges">
29+
<button class="dropbtn">{% trans "Period Range" %}</button>
30+
<div class="dropdown-content">
31+
<button data-period="day" data-view="current">{% trans "Day View" %}</button>
32+
<button data-period="week" data-view="current">{% trans "Week View" %}</button>
33+
<button data-period="month" data-view="current">{% trans "Month View" %}</button>
34+
<button data-period="year" data-view="current">{% trans "Year View" %}</button>
35+
</div>
36+
</div>
37+
<div class="dropdown preset-ranges">
38+
<button class="dropbtn">{% trans "Preset Ranges" %}</button>
39+
<div class="dropdown-content">
40+
<button data-period="day" data-view="yesterday">{% trans "Yesterday" %}</button>
41+
<button data-period="week" data-view="last">{% trans "Last Week" %}</button>
42+
<button data-period="month" data-view="last">{% trans "Last Month" %}</button>
43+
<button data-period="month" data-view="last" data-amount="3">{% trans "Last 3 Months" %}</button>
44+
<button data-period="month" data-view="last" data-amount="6">{% trans "Last 6 Months" %}</button>
45+
<button data-period="year" data-view="last">{% trans "Last Year" %}</button>
46+
</div>
47+
</div>
48+
<button class="arrow-btn" data-direction="prev">&laquo;</button>
49+
<button class="arrow-btn" data-direction="next">&raquo;</button>
50+
</div>
51+
52+
53+
<script>
54+
$(function () {
55+
initDatepicker({
56+
formId: '{{ datepicker_id }}',
57+
startFieldName: '{{ form.start.name }}',
58+
endFieldName: '{{ form.end.name }}'
59+
});
60+
});
61+
</script>

0 commit comments

Comments
 (0)