Compare commits
No commits in common. "33293d2ba8ea3a531db2b40662603274b0af91e1" and "081b094a3d8cb9119a399fb214c4161a5c645a4c" have entirely different histories.
33293d2ba8
...
081b094a3d
Binary file not shown.
|
@ -318,10 +318,6 @@ msgstr "És el primer cop que accedeixes, et donem la benvinguda!"
|
||||||
msgid " The disk space of resources is updated weekly "
|
msgid " The disk space of resources is updated weekly "
|
||||||
msgstr "L'espai en disc dels recursos es va actualitzant setmanalment."
|
msgstr "L'espai en disc dels recursos es va actualitzant setmanalment."
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:47
|
|
||||||
msgid "Show history"
|
|
||||||
msgstr "Mostrar historial"
|
|
||||||
|
|
||||||
#: templates/musician/database_list.html:21
|
#: templates/musician/database_list.html:21
|
||||||
#: templates/musician/mailbox_list.html:30 templates/musician/saas_list.html:19
|
#: templates/musician/mailbox_list.html:30 templates/musician/saas_list.html:19
|
||||||
#: templates/musician/webapps/webapp_list.html:25
|
#: templates/musician/webapps/webapp_list.html:25
|
||||||
|
|
Binary file not shown.
|
@ -320,10 +320,6 @@ msgstr "Es la primera vez que accedes: ¡te damos la bienvenida!"
|
||||||
msgid " The disk space of resources is updated weekly "
|
msgid " The disk space of resources is updated weekly "
|
||||||
msgstr "El espacio en disco de los recursos se actualiza semanalmente"
|
msgstr "El espacio en disco de los recursos se actualiza semanalmente"
|
||||||
|
|
||||||
#: templates/musician/dashboard.html:47
|
|
||||||
msgid "Show history"
|
|
||||||
msgstr "Mostrar historial"
|
|
||||||
|
|
||||||
#: templates/musician/database_list.html:21
|
#: templates/musician/database_list.html:21
|
||||||
#: templates/musician/mailbox_list.html:30 templates/musician/saas_list.html:19
|
#: templates/musician/mailbox_list.html:30 templates/musician/saas_list.html:19
|
||||||
#: templates/musician/webapps/webapp_list.html:25
|
#: templates/musician/webapps/webapp_list.html:25
|
||||||
|
|
|
@ -37,22 +37,10 @@
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for name, obj_data in account.objects.items %}
|
{% for name, obj_data in account.objects.items %}
|
||||||
{% if obj_data.ac != None %}
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
{{ name }}
|
||||||
<div class="row w-100 justify-content-between">
|
<span class="badge badge-primary badge-pill">{{ obj_data.ac.used }} {{ obj_data.ac.unit }}</span>
|
||||||
<div class="col-4">
|
</li>
|
||||||
{{ name }}
|
|
||||||
</div>
|
|
||||||
<div class="col-4 text-center">
|
|
||||||
<a href="{% url 'musician:dashboard-history' obj_data.ac.id %}" target="_blank">{% trans "Show history" %} <i class="fas fa-clock"></i></a>
|
|
||||||
</div>
|
|
||||||
<div class="col-3"></div>
|
|
||||||
<div class="col-1">
|
|
||||||
<span class="badge badge-primary badge-pill">{{ obj_data.ac.used }} {{ obj_data.ac.unit }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,251 +0,0 @@
|
||||||
{% load i18n utils static %}
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Resource history</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/dancing-dots.css" %}"/>
|
|
||||||
<script src="{% static "admin/js/jquery.js" %}" type="text/javascript"></script>
|
|
||||||
<script src="{% static "orchestra/js/highcharts/stock/highstock.js" %}" type="text/javascript"></script>
|
|
||||||
<script src="{% static "orchestra/js/highcharts/modules/exporting.js" %}" type="text/javascript"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
String.prototype.capitalize = function() {
|
|
||||||
return this.charAt(0).toUpperCase() + this.slice(1);
|
|
||||||
}
|
|
||||||
function plot_charts(url) {
|
|
||||||
charts = {
|
|
||||||
series: function (div, i, seriesOptions, resource) {
|
|
||||||
$(div).highcharts('StockChart', {
|
|
||||||
chart: {
|
|
||||||
backgroundColor: (i % 2 ? "#EDF3FE" : "#FFFFFF")
|
|
||||||
},
|
|
||||||
rangeSelector: {
|
|
||||||
selected: 4
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: resource['content_type'].capitalize() + ' ' +
|
|
||||||
resource['verbose_name'].toLowerCase() + ' ' +
|
|
||||||
resource['aggregation'] +
|
|
||||||
(div.indexOf('aggregate') > 0 ? ' (aggregated)': '')
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
ordinal: false
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function () {
|
|
||||||
return this.value + ' ' + resource['unit'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plotLines: [{
|
|
||||||
value: 0,
|
|
||||||
width: 2,
|
|
||||||
color: 'silver'
|
|
||||||
}],
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
/*legend: {
|
|
||||||
align: 'right',
|
|
||||||
x: -30,
|
|
||||||
verticalAlign: 'top',
|
|
||||||
y: 25,
|
|
||||||
floating: true,
|
|
||||||
backgroundColor: (Highcharts.theme && Highcharts.theme.background2) || 'white',
|
|
||||||
borderColor: '#CCC',
|
|
||||||
borderWidth: 1,
|
|
||||||
shadow: false,
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
rangeSelector: {
|
|
||||||
enabled: false
|
|
||||||
},*/
|
|
||||||
tooltip: {
|
|
||||||
pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y:.3f} ' +
|
|
||||||
resource['unit']+ '</b><br/>',
|
|
||||||
valueDecimals: 3
|
|
||||||
},
|
|
||||||
series: seriesOptions
|
|
||||||
});
|
|
||||||
},
|
|
||||||
columns: function (div, i, seriesOptions, resource){
|
|
||||||
$(div).highcharts({
|
|
||||||
chart: {
|
|
||||||
type: 'column',
|
|
||||||
backgroundColor: (i % 2 ? "#EDF3FE" : "#FFFFFF")
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: resource['content_type'].capitalize() + ' ' +
|
|
||||||
resource['verbose_name'].toLowerCase() + ' ' +
|
|
||||||
resource['aggregation'] +
|
|
||||||
(div.indexOf('aggregate') > 0 ? ' (aggregated)': '')
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
categories: resource['dates']
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
min: 0,
|
|
||||||
title: {
|
|
||||||
text: resource['unit']
|
|
||||||
},
|
|
||||||
stackLabels: {
|
|
||||||
enabled: true,
|
|
||||||
style: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: (Highcharts.theme && Highcharts.theme.textColor) || 'gray'
|
|
||||||
},
|
|
||||||
formatter: function () {
|
|
||||||
return this.total + ' ' + resource['unit'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
align: 'right',
|
|
||||||
x: -30,
|
|
||||||
verticalAlign: 'top',
|
|
||||||
y: 25,
|
|
||||||
floating: true,
|
|
||||||
backgroundColor: (Highcharts.theme && Highcharts.theme.background2) || 'white',
|
|
||||||
borderColor: '#CCC',
|
|
||||||
borderWidth: 1,
|
|
||||||
shadow: false
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
formatter: function () {
|
|
||||||
var s = ['<b>' + this.x + '</b>'];
|
|
||||||
$.each(this.points, function(i, point) {
|
|
||||||
s.push('<span style="color:' + this.series.color + '">' + this.series.name + ': ' + this.y + ' ' + resource['unit']);
|
|
||||||
});
|
|
||||||
s.push('<b>Total: ' + this.points[0].total + ' ' + resource['unit'] + '</b>');
|
|
||||||
return s.join('<br>');
|
|
||||||
},
|
|
||||||
valueDecimals: 3,
|
|
||||||
shared: true
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
column: {
|
|
||||||
stacking: 'normal',
|
|
||||||
dataLabels: {
|
|
||||||
enabled: true,
|
|
||||||
color: (Highcharts.theme && Highcharts.theme.dataLabelsColor) || 'white',
|
|
||||||
style: {
|
|
||||||
textShadow: '0 0 3px black'
|
|
||||||
},
|
|
||||||
formatter: function () {
|
|
||||||
return this.series.name + ': ' + this.y.toFixed(3) + ' ' + resource['unit'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: seriesOptions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.getJSON(url, function(data) {
|
|
||||||
var dataLength = data.length;
|
|
||||||
$("#charts").empty();
|
|
||||||
for (i=0; i < dataLength; i++) {
|
|
||||||
var index = 0;
|
|
||||||
var seriesOptions = [];
|
|
||||||
var resource = data[i];
|
|
||||||
var objects = resource["objects"];
|
|
||||||
var objectsLength = objects.length;
|
|
||||||
var a_index = 0;
|
|
||||||
var aggregated = false;
|
|
||||||
var aggregates = []
|
|
||||||
for (j=0; j < objectsLength; j++) {
|
|
||||||
aggregate = [];
|
|
||||||
var object = objects[j];
|
|
||||||
var monitors = object["monitors"];
|
|
||||||
var monitorsLength = monitors.length;
|
|
||||||
for (k=0; k < monitorsLength; k++) {
|
|
||||||
var datasets = monitors[k]['datasets'];
|
|
||||||
if (resource['aggregated_history']) {
|
|
||||||
var datasetsLength = datasets.length;
|
|
||||||
for (l=0; l < datasetsLength; l++) {
|
|
||||||
seriesOptions.push(datasets[l]);
|
|
||||||
for (m=0; m < resource['dates'].length; m++) {
|
|
||||||
aggregate[m] = ((aggregate[m] || 0 ) + datasets[l]['data'][m]);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
for (var object_name in datasets) {
|
|
||||||
seriesOptions[index] = {
|
|
||||||
name: object_name,
|
|
||||||
data: datasets[object_name]
|
|
||||||
};
|
|
||||||
index += 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (k > 1)
|
|
||||||
aggregated = true;
|
|
||||||
aggregates[a_index] = {
|
|
||||||
name: object['object_name'],
|
|
||||||
data: aggregate
|
|
||||||
};
|
|
||||||
a_index += 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
divs = (
|
|
||||||
'<div class="chart-box" style="background: '+(i % 2 ? "#EDF3FE" : "#FFFFFF")+';">' +
|
|
||||||
'<h1>'+resource['content_type'].capitalize() + ' ' + resource['verbose_name'].toLowerCase() + '</h1>' +
|
|
||||||
'<div id="resource-'+i+'" class="chart"></div>'
|
|
||||||
);
|
|
||||||
if (a_index > 1 && aggregated && resource['aggregated_history'])
|
|
||||||
divs += '<br><div class="chart" id="resource-'+i+'-aggregate"></div>';
|
|
||||||
divs += '</div>';
|
|
||||||
|
|
||||||
$("#charts").append(divs);
|
|
||||||
if (a_index > 1 && aggregated && resource['aggregated_history'])
|
|
||||||
charts['columns']('#resource-'+i+'-aggregate', i, aggregates, resource);
|
|
||||||
charts[(resource['aggregated_history'] ? 'columns': 'series')]('#resource-'+i, i, seriesOptions, resource);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
plot_charts("{% url 'musician:dashboard-historydata' ids %}");
|
|
||||||
</script>
|
|
||||||
<style type="text/css">
|
|
||||||
@page {
|
|
||||||
size: 11.69in 8.27in;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family: sans;
|
|
||||||
font-size: 21px;
|
|
||||||
}
|
|
||||||
#notice {
|
|
||||||
font-family: sans;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
#message {
|
|
||||||
width:300px;
|
|
||||||
margin:0 auto;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-top: 5%;
|
|
||||||
}
|
|
||||||
.chart-box {
|
|
||||||
margin: 10px;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border: 1px solid grey;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.chart {
|
|
||||||
height: 400px;
|
|
||||||
min-width: 310px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="notice">♦Notice that resources used by deleted services will not appear.</div>
|
|
||||||
<div id="charts">
|
|
||||||
<div id="message">
|
|
||||||
> crunching data <span id="dancing-dots-text"> <span><span>.</span><span>.</span><span>.</span></span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -17,8 +17,6 @@ urlpatterns = [
|
||||||
path('auth/login/', views.LoginView.as_view(), name='login'),
|
path('auth/login/', views.LoginView.as_view(), name='login'),
|
||||||
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
||||||
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||||
path('dashboard/historydata/<int:pk>/', views.HistoryDataView.as_view(), name='dashboard-historydata'),
|
|
||||||
path('dashboard/history/<int:pk>/', views.HistoryView.as_view(), name='dashboard-history'),
|
|
||||||
|
|
||||||
path('domains/', views.DomainListView.as_view(), name='domain-list'),
|
path('domains/', views.DomainListView.as_view(), name='domain-list'),
|
||||||
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
|
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.db.models import Value
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
from django.http import (HttpResponse, HttpResponseNotFound,
|
from django.http import (HttpResponse, HttpResponseNotFound,
|
||||||
HttpResponseRedirect)
|
HttpResponseRedirect)
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
@ -59,57 +59,6 @@ from .lists.views import *
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import json
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
from orchestra.contrib.resources.helpers import get_history_data
|
|
||||||
from django.http import HttpResponseNotFound, Http404
|
|
||||||
|
|
||||||
class HistoryView(CustomContextMixin, UserTokenRequiredMixin, View):
|
|
||||||
|
|
||||||
def check_resource(self, pk):
|
|
||||||
related_resources = self.get_all_resources()
|
|
||||||
|
|
||||||
account = related_resources.filter(resource_id__verbose_name='account-disk').first()
|
|
||||||
account_trafic = related_resources.filter(resource_id__verbose_name='account-traffic').first()
|
|
||||||
account = getattr(account, "id", False) == pk
|
|
||||||
account_trafic = getattr(account_trafic, "id", False) == pk
|
|
||||||
if account == False and account_trafic == False:
|
|
||||||
raise Http404(f"Resource with id {pk} does not exist")
|
|
||||||
|
|
||||||
|
|
||||||
def get(self, request, pk, *args, **kwargs):
|
|
||||||
context = {
|
|
||||||
'ids': pk
|
|
||||||
}
|
|
||||||
self.check_resource(pk)
|
|
||||||
return render(request, "musician/history.html", context)
|
|
||||||
|
|
||||||
# TODO: funcion de dashborad, mirar como no repetir esta funcion
|
|
||||||
def get_all_resources(self):
|
|
||||||
user = self.request.user
|
|
||||||
resources = Resource.objects.select_related('content_type')
|
|
||||||
resource_models = {r.content_type.model_class(): r.content_type_id for r in resources}
|
|
||||||
ct_id = resource_models[user._meta.model]
|
|
||||||
qset = Q(content_type_id=ct_id, object_id=user.id, resource__is_active=True)
|
|
||||||
for field, rel in user._meta.fields_map.items():
|
|
||||||
try:
|
|
||||||
ct_id = resource_models[rel.related_model]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
manager = getattr(user, field)
|
|
||||||
ids = manager.values_list('id', flat=True)
|
|
||||||
qset = Q(qset) | Q(content_type_id=ct_id, object_id__in=ids, resource__is_active=True)
|
|
||||||
return ResourceData.objects.filter(qset)
|
|
||||||
|
|
||||||
|
|
||||||
class HistoryDataView(CustomContextMixin, UserTokenRequiredMixin, View):
|
|
||||||
def get(self, request, pk, *args, **kwargs):
|
|
||||||
ids = [pk]
|
|
||||||
queryset = ResourceData.objects.filter(id__in=ids)
|
|
||||||
history = get_history_data(queryset)
|
|
||||||
response = json.dumps(history, indent=4)
|
|
||||||
return HttpResponse(response, content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
@ -127,6 +76,10 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
account = related_resources.filter(resource_id__verbose_name='account-disk').first()
|
account = related_resources.filter(resource_id__verbose_name='account-disk').first()
|
||||||
account_trafic = related_resources.filter(resource_id__verbose_name='account-traffic').first()
|
account_trafic = related_resources.filter(resource_id__verbose_name='account-traffic').first()
|
||||||
|
|
||||||
|
# TODO: sacar los graficos de alguna manera
|
||||||
|
# url_history_disk = reverse('admin:resources_resourcedata_show_history', args=(account.pk,))
|
||||||
|
# url_history_traffic = reverse('admin:resources_resourcedata_show_history', args=(account_trafic.pk,))
|
||||||
|
|
||||||
mailboxes = related_resources.filter(resource_id__verbose_name='mailbox-disk')
|
mailboxes = related_resources.filter(resource_id__verbose_name='mailbox-disk')
|
||||||
lists = related_resources.filter(resource_id__verbose_name='list-traffic')
|
lists = related_resources.filter(resource_id__verbose_name='list-traffic')
|
||||||
databases = related_resources.filter(resource_id__verbose_name='database-disk')
|
databases = related_resources.filter(resource_id__verbose_name='database-disk')
|
||||||
|
@ -142,6 +95,7 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
|
||||||
# TODO(@slamora) update when backend provides resource usage data
|
# TODO(@slamora) update when backend provides resource usage data
|
||||||
resource_usage = {
|
resource_usage = {
|
||||||
|
# 'account': self.get_account_usage(profile_type, account),
|
||||||
'mailbox': self.get_resource_usage(profile_type, mailboxes, 'mailbox'),
|
'mailbox': self.get_resource_usage(profile_type, mailboxes, 'mailbox'),
|
||||||
'database': self.get_resource_usage(profile_type, databases, 'database'),
|
'database': self.get_resource_usage(profile_type, databases, 'database'),
|
||||||
'nextcloud': self.get_resource_usage(profile_type, nextcloud, 'nextcloud'),
|
'nextcloud': self.get_resource_usage(profile_type, nextcloud, 'nextcloud'),
|
||||||
|
@ -187,7 +141,7 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
rs_left = 0
|
rs_left = 0
|
||||||
alert = ''
|
alert = ''
|
||||||
progres_bar = False
|
progres_bar = False
|
||||||
|
|
||||||
if ALLOWED_RESOURCES[profile_type].get(name_resource):
|
if ALLOWED_RESOURCES[profile_type].get(name_resource):
|
||||||
progres_bar = True
|
progres_bar = True
|
||||||
limit_rs = ALLOWED_RESOURCES[profile_type][name_resource]
|
limit_rs = ALLOWED_RESOURCES[profile_type][name_resource]
|
||||||
|
@ -214,19 +168,15 @@ class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_account_usage(self, profile_type, account, account_trafic):
|
def get_account_usage(self, profile_type, account, account_trafic):
|
||||||
total_size = 0
|
|
||||||
if account != None and getattr(account, "used") != None:
|
|
||||||
total_size = account.used
|
|
||||||
|
|
||||||
allowed_size = ALLOWED_RESOURCES[profile_type]['account']
|
allowed_size = ALLOWED_RESOURCES[profile_type]['account']
|
||||||
|
total_size = account.used
|
||||||
size_left = allowed_size - total_size
|
size_left = allowed_size - total_size
|
||||||
unit = account.unit if account != None else "GiB"
|
|
||||||
|
|
||||||
alert = ''
|
alert = ''
|
||||||
if size_left < 0:
|
if size_left < 0:
|
||||||
alert = format_html(f"<span class='text-danger'>{size_left * -1} {unit} extra</span>")
|
alert = format_html(f"<span class='text-danger'>{size_left * -1} {account.unit} extra</span>")
|
||||||
elif size_left <= 1:
|
elif size_left <= 1:
|
||||||
alert = format_html(f"<span class='text-warning'>{size_left} {unit} available</span>")
|
alert = format_html(f"<span class='text-warning'>{size_left} {account.unit} available</span>")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'verbose_name': _('Account'),
|
'verbose_name': _('Account'),
|
||||||
|
|
Loading…
Reference in New Issue