Initial Upload

This commit is contained in:
Matt Batchelder
2025-12-02 10:32:59 -05:00
commit 05ce0da296
2240 changed files with 467811 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
{% if metadata.logType == 'audit' %}
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Entity" %}</th>
<th>{% trans "Entity ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Details" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.logDate }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.userId }}</td>
<td>{{ item.applicationName }}</td>
<td>{{ item.requestId }}</td>
<td>{{ item.method }}</td>
<td>{{ item.url }}</td>
<td>{{ item.entity }}</td>
<td>{{ item.entityId }}</td>
<td>{{ item.message }}</td>
<td>{{ item.objectAfter }}</td>
</tr>
{% endfor %}
{% elseif metadata.logType == 'debug' %}
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Details" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.logDate }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.userId }}</td>
<td>{{ item.applicationName }}</td>
<td>{{ item.requestId }}</td>
<td>{{ item.method }}</td>
<td>{{ item.url }}</td>
<td>{{ item.type }}</td>
<td>{{ item.message }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.startTime }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.userId }}</td>
<td>{{ item.applicationName }}</td>
<td>{{ item.requestId }}</td>
<td>{{ item.method }}</td>
<td>{{ item.url }}</td>
</tr>
{% endfor %}
{% endif %}
</table>
<br/>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
{% endblock %}

View File

@@ -0,0 +1,390 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: API Requests History" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "API Requests History" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom">
<div class="FilterDiv card-body" id="apiRequestsHistoryFilter">
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{{ inline.dateRangeFilter("reportFilter", title, "", "", "", "", "") }}
{% set title %}{% trans "User" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Request ID" %}{% endset %}
{{ inline.number('requestId', title) }}
{% set title = "Report Type"|trans %}
{% set options = [
{ id: 'requests', value: "Requests"|trans },
{ id: 'audit', value: "Audit"|trans },
{ id: 'debug', value: "Debug"|trans },
] %}
{{ inline.dropdown("type", "single", title, "requests", options, "id", "value", helpText) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Body -->
<div class="card-body api-requests-history-audit">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="apiRequestsHistoryAuditGrid"
class="table xibo-table table-striped"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'apirequests'}) }}">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Entity" %}</th>
<th>{% trans "Entity ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="card-body api-requests-history-log d-none">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="apiRequestsHistoryLogGrid"
class="table xibo-table table-striped"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'apirequests'}) }}">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="card-body api-requests-history d-none">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="apiRequestsHistoryGrid"
class="table xibo-table table-striped"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'apirequests'}) }}">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{% verbatim %}
<script type="text/x-handlebars-template" id="table-array-viewer">
<a class="arrayViewerToggle" href="#"><span class="fa fa-search"></span></a>
<table class="arrayViewer table table-bordered">
<thead>
<tr>
<th>{{ col1 }}</th>
<th>{{ col2 }}</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{ @key }}</td>
<td>{{ this }}</td>
</tr>
{{/each}}
</tbody>
</table>
</script>
{% endverbatim %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(function() {
var arrayViewer = Handlebars.compile($('#table-array-viewer').html());
$('[data-toggle="popover"]').popover();
let $report = $('#apiRequestsHistoryFilter');
let $auditDataTable = $('#apiRequestsHistoryAuditGrid');
let auditTable = createAuditTable($auditDataTable);
let $logDataTable = $('#apiRequestsHistoryLogGrid');
let logTable = createLogTable($logDataTable);
let $requestsDataTable = $('#apiRequestsHistoryGrid');
let requestsTable = createRequestsTable($requestsDataTable);
let result; // XHR get data result
let reportType = $report.find('#type').val();
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find('form').serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
if (reportType === 'audit') {
$('.api-requests-history-audit').removeClass('d-none');
$('.api-requests-history-log').addClass('d-none');
$('.api-requests-history').addClass('d-none');
setTabularData(auditTable, result.table);
} else if (reportType === 'debug') {
$('.api-requests-history-log').removeClass('d-none');
$('.api-requests-history-audit').addClass('d-none');
$('.api-requests-history').addClass('d-none');
setTabularData(logTable, result.table);
} else {
$('.api-requests-history').removeClass('d-none');
$('.api-requests-history-audit').addClass('d-none');
$('.api-requests-history-log').addClass('d-none');
setTabularData(requestsTable, result.table);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
if ($report.find('#type').val() === 'audit') {
$('.arrayViewerToggle').click(function () {
$(this).parent().find('.arrayViewer').toggle();
});
}
table.columns.adjust().draw();
}
// Apply
$applyBtn.click(function () {
reportType = $report.find('#type').val();
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($auditDataTable.data().url);
});
$("#refreshGrid").click(function () {
reportType = $report.find('#type').val();
if (reportType === 'audit') {
auditTable.ajax.reload();
} else if (reportType === 'debug') {
logTable.ajax.reload();
} else {
requestsTable.ajax.reload();
}
});
function createAuditTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
responsive: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
{data: 'entity', responsivePriority: 2},
{
name: 'entityId',
responsivePriority: 2,
data : function (data) {
if (data.entityId === 0) {
return ''
}
return data.entityId;
}
},
{data: 'message'},
{
"data": function (data, type, row, meta) {
if (type != "display")
return "";
return arrayViewer(
{"col1": "{% trans "Property" %}", "col2": "{% trans "Value" %}", "items": data.objectAfter}
);
},
"sortable": false,
responsivePriority: 1
}
]
})
}
function createLogTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
responsive: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
{data: 'type'},
{data: 'message'},
]
})
}
function createRequestsTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
responsive: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'startTime'},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
]
})
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,216 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
{% if metadata.logType == 'audit' %}
<div class="XiboData card pt-3">
<table id="apiRequestsHistoryAuditReportPreview" class="table table-striped">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Entity" %}</th>
<th>{% trans "Entity ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% elseif metadata.logType == 'debug' %}
<div class="XiboData card pt-3">
<table id="apiRequestsHistoryDebugReportPreview" class="table table-striped">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% else %}
<div class="XiboData card pt-3">
<table id="apiRequestsHistoryReportPreview" class="table table-striped">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "Application" %}</th>
<th>{% trans "Request ID" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Url" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(function() {
const outputData = {{ table|json_encode|raw }};
const type = '{{ metadata.logType }}';
if (type === 'audit') {
const auditTable = $("#apiRequestsHistoryAuditReportPreview").DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
paging: false,
ordering: false,
info: false,
order: [[0, 'desc']],
searching: false,
data: outputData,
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
{data: 'entity'},
{
name: 'entityId',
responsivePriority: 2,
data : function (data) {
if (data.entityId === 0) {
return ''
}
return data.entityId;
}
},
{data: 'message'},
{data: 'objectAfter'}
]
});
auditTable.on('draw', dataTableDraw);
auditTable.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
} else if (type === 'debug') {
const debugTable = $("#apiRequestsHistoryDebugReportPreview").DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
paging: false,
ordering: false,
info: false,
order: [[0, 'desc']],
searching: false,
data: outputData,
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
{data: 'type'},
{data: 'message'},
]
});
debugTable.on('draw', dataTableDraw);
debugTable.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
} else {
const requestsTable = $("#apiRequestsHistoryReportPreview").DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
paging: false,
ordering: false,
info: false,
order: [[0, 'desc']],
searching: false,
data: outputData,
columns: [
{data: 'startTime'},
{data: 'userName'},
{data: 'userId'},
{data: 'applicationName'},
{data: 'requestId'},
{data: 'method'},
{data: 'url'},
]
});
requestsTable.on('draw', dataTableDraw);
requestsTable.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,95 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "User" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
] %}
{{ forms.dropdown("userId", "single", title, "", null, "userId", "userName", "", "pagedSelect", "", "d", "", attributes) }}
{% set title = "Report Type"|trans %}
{% set options = [
{ id: 'requests', value: "Requests"|trans },
{ id: 'audit', value: "Audit"|trans },
{ id: 'debug', value: "Debug"|trans },
] %}
{{ forms.dropdown("type", "single", title, "audit", options, "id", "value", helpText) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "apirequests",
"description": "API Requests History",
"class": "\\Xibo\\Report\\ApiRequests",
"type": "Report",
"output_type": "table",
"color":"orange",
"fa_icon": "fa-th",
"sort_order": 5,
"hidden": 0,
"category": "Audit",
"feature": "admin",
"adminOnly": 1
}

View File

@@ -0,0 +1,48 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
<p></p>
<table class="saved-report-table">
<tr>
<th></th>
<th>{% trans "Bandwidth" %}</th>
<th>{% trans "Unit" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.label }}</td>
<td>{{ item.bandwidth }}</td>
<td>{{ item.unit }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,251 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Bandwidth" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Bandwidth" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="bandwidthReport">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.dateMonth("bandwidthFromDt", title, defaults.toDate, "", "", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.dateMonth("bandwidthToDt", title, defaults.toDate, "", "", "", "") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
aria-controls="chartTab" aria-selected="true">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="false">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- CHART TAB-->
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
<div class="chart-container" style="height:550px;">
<canvas id="canvas" style="clear:both; margin-top:25px;" height="70%"></canvas>
</div>
</div>
<!-- TABULAR TAB-->
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="bandwidthTbl"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="/report/data/bandwidth"
>
<thead>
<tr>
<th></th>
<th>Bandwidth</th>
<th>Unit</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
let $report = $("#bandwidthReport");
let $dataTable = $('#bandwidthTbl'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $applyBtn = $("#applyBtn");
// Initialize table with empty data
let table = $dataTable.DataTable({
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
ordering: false,
data: {},
columns: [
{
data: 'label',
'sortable': false,
},
{
data: 'bandwidth',
'sortable': false,
},
{
data: 'unit',
'sortable': false,
}
],
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find("form").serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($dataTable.data().url);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,95 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<canvas id="canvas" style="clear:both; margin-top:25px;"></canvas>
</div>
<br/>
<div class="XiboData card pt-3">
<table id="bandwidthTbl" class="table table-striped">
<thead>
<tr>
<th></th>
<th>{% trans "Bandwidth" %}</th>
<th>{% trans "Unit" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
let reportChart = new Chart($("#canvas"), {{ chart|json_encode|raw }});
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#bandwidthTbl").DataTable({
"searching": false,
"paging": true,
"ordering": false,
"data": outputData,
"columns": [
{ "data": 'label' },
{ "data": 'bandwidth' },
{ "data": 'unit' },
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
</script>
{% endblock %}

View File

@@ -0,0 +1,90 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

14
reports/bandwidth.report Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "bandwidth",
"description": "Display Statistics: Bandwidth",
"class": "\\Xibo\\Report\\Bandwidth",
"type": "Chart",
"output_type": "chart",
"color":"green",
"fa_icon": "fa-bar-chart",
"sort_order": 3,
"hidden": 0,
"category": "Display",
"feature": "displays.reporting",
"adminOnly": 0
}

31
reports/base-report.twig Normal file
View File

@@ -0,0 +1,31 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% if logo %}
<div>
<img class="logo right" src="{{ logo }}">
</div>
{% endif %}
<div class="header">{{ header }}</div>
<div class="title">{{ title }} <span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span></div>
{% block content %}{% endblock %}

View File

@@ -0,0 +1,48 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Ad Plays" %}</th>
<th>{% trans "Ad Duration" %}</th>
<th>{% trans "Audience Impressions" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.labelDate }}</td>
<td>{{ item.adPlays }}</td>
<td>{{ item.adDuration }}</td>
<td>{{ item.impressions }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,529 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Campaign Proof of Play" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Campaign Proof of Play" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="campaignProofOfPlayView" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="campaignProofOfPlayReport">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set wholecampaign %}{% trans "Whole Campaign" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "wholecampaign", reportFilter: wholecampaign },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set hour %}{% trans "Hour" %}{% endset %}
{% set day %}{% trans "Day" %}{% endset %}
{% set week %}{% trans "Week" %}{% endset %}
{% set month %}{% trans "Month" %}{% endset %}
{% set options = [
{ name: "hour", filter: hour },
{ name: "day", filter: day },
{ name: "week", filter: week },
{ name: "month", filter: month },
] %}
{{ inline.dropdown("groupBy", "single", title, "day", options, "name", "filter") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{# Campaign list only. #}
{% set attributes = [
{ name: "data-search-url", value: url_for("campaign.search") },
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
] %}
{% set title %}{% trans "Campaign" %}{% endset %}
{% set helpText %}{% trans "Please select a Campaign" %}{% endset %}
{{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="true">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- TABULAR TAB-->
<div class="tab-pane active" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="stats"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-state-preference-name="proofOfPlayGrid"
data-url="/report/data/campaignProofOfPlay">
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Ad Plays" %}</th>
<th>{% trans "Ad Duration" %}</th>
<th>{% trans "Audience Impressions" %}</th>
<th>{% trans "Spend" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let $dataTable = $('#stats'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Report Filter
let reportFilter = $("#reportFilter"); // Report Filter
// Grid
let table = $dataTable.DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
stateSave: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
drawCallback: function( settings ) {
setTimeout(function() {
$("#applyBtn").removeClass('disabled');
}, 300);
},
filter: false,
"order": [[0, "asc"]],
data:{},
"columns": [
{"data": "labelDate"},
{"data": "adPlays"},
{"data": "adDuration"},
{
"data": "impressions",
"render": dataTableRoundDecimal
},
{
"data": "spend",
"render": dataTableRoundDecimal
},
],
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Total over all pages
let totalAdPlays = api.column(1).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalAdPlaysCurrentPage = api.column(1, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalAdDuration = api.column(2).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalAdDurationCurrentPage = api.column(2, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalImpression = api.column(3).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalImpressionCurrentPage = api.column(3, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalSpend = api.column(4).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalSpendCurrentPage = api.column(4, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
// Update footer
$(api.column(1).footer()).html(totalAdPlaysCurrentPage + ' (' + totalAdPlays + ' total)');
$(api.column(2).footer()).html(totalAdDurationCurrentPage + ' (' + totalAdDuration + ' total)');
$(api.column(3).footer()).html(parseFloat(totalImpressionCurrentPage).toFixed(2) + ' (' + parseFloat(totalImpression).toFixed(2) + ' total)');
$(api.column(4).footer()).html(parseFloat(totalSpendCurrentPage).toFixed(2) + ' (' + parseFloat(totalSpend).toFixed(2) + ' total)');
},
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $("#stats").closest(".XiboGrid").find(".FilterDiv form").serializeObject(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
if (result.error) {
toastr.error(result.error);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
if (reportFilter.val() === '' || reportFilter.val() === undefined) {
$(".stats-from-dt").show();
$(".stats-to-dt").show();
} else {
$(".stats-from-dt").hide();
$(".stats-to-dt").hide();
}
}
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#statsFromDt").val());
let toDt = moment($("#statsToDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#statsFromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#statsToDtLink").change( function() {
calculateDaysShowHideWarn();
});
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
let anchorReportAddBtn = $("button#reportAddBtn");
anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=campaignProofOfPlay" );
};
checkReportFilter(reportFilter);
checkFilterAndApply();
var $campaignSelect = $('#parentCampaignId');
$campaignSelect.select2({
ajax: {
url: $campaignSelect.data("searchUrl"),
dataType: "json",
delay: 250,
placeholder: 'Campaign',
allowClear: true,
data: function(params) {
var query = {
isLayoutSpecific: 0,
retired: 0,
totalDuration: 0,
name: params.term,
start: 0,
length: 10,
columns: [
{
"data": "isLayoutSpecific"
},
{
"data": "campaign"
}
],
order: [
{
"column": 0,
"dir": "asc"
},
{
"column": 1,
"dir": "asc"
}
]
};
// Set the start parameter based on the page number
if (params.page != null) {
query.start = (params.page - 1) * 10;
}
return query;
},
processResults: function(data, params) {
var results = [];
var campaigns = [];
$.each(data.data, function(index, element) {
campaigns.push({
"id": element.campaignId,
"text": element.campaign
});
});
results.push({
"text": $campaignSelect.data('transCampaigns'),
"children": campaigns
})
var page = params.page || 1;
page = (page > 1) ? page - 1 : page;
return {
results: results,
pagination: {
more: (page * 10 < data.recordsTotal)
}
}
}
}
})
});
function campaignProofOfPlayScheduleCallback() {
let $displayId = $('#campaignProofOfPlayReport #displayId');
let $newDisplayId = $('#campaignProofofplayScheduleAddForm #displayId');
appendOptions($newDisplayId, $displayId.find('option:selected').clone());
}
function appendOptions(element, options) {
for (let i = 0; i < options.length; i++) {
let option = options[i];
element.append(option).trigger('change');
$(option).prop('selected', true);
element.trigger({
type: 'select2:select',
params: {
data: option
}
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>Period</th>
<th>Ad Plays</th>
<th>Ad Duration</th>
<th>Audience Impressions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
"data": outputData,
"columns": [
{"data": "labelDate"},
{"data": "adPlays"},
{"data": "adDuration"},
{"data": "impressions"}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#campaignProofofplayScheduleAddForm").submit()
{% endblock %}
{% block callBack %}campaignProofOfPlayScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="campaignProofofplayScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-default-values", value: displayId },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,516 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Display Ad Plays" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Display Ad Plays" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayAdPlayView" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="report">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set wholecampaign %}{% trans "Whole Campaign" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "wholecampaign", reportFilter: wholecampaign },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("fromDt", title, defaults.fromDateOneDay, "", "from-dt", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("toDt", title, defaults.toDate, "", "to-dt", "", "") }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set hour %}{% trans "Hour" %}{% endset %}
{% set day %}{% trans "Day" %}{% endset %}
{% set week %}{% trans "Week" %}{% endset %}
{% set month %}{% trans "Month" %}{% endset %}
{% set options = [
{ name: "hour", filter: hour },
{ name: "day", filter: day },
{ name: "week", filter: week },
{ name: "month", filter: month },
] %}
{{ inline.dropdown("groupBy", "single", title, "today", options, "name", "filter") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{# Campaign list only. #}
{% set attributes = [
{ name: "data-search-url", value: url_for("campaign.search") },
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
] %}
{% set title %}{% trans "Campaign" %}{% endset %}
{% set helpText %}{% trans "Please select a Campaign" %}{% endset %}
{{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }}
{% set title %}{% trans "Layout" %}{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
aria-controls="chartTab" aria-selected="true">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="false">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- CHART TAB-->
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
<div class="chart-container" style="height:550px;">
<canvas id="canvas" style="clear:both; margin-top:25px;" height="70%"></canvas>
<img id="imageLoader" style="display: block; margin: auto;" src="{{ theme.uri("img/loader.gif") }}">
</div>
</div>
<!-- TABULAR TAB-->
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="stats"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-state-preference-name="displayAdPlayGrid"
data-url="/report/data/displayAdPlay">
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Ad Plays" %}</th>
<th>{% trans "Impressions" %}</th>
<th>{% trans "Spend" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let $dataTable = $('#stats'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Report Filter
let reportFilter = $("#reportFilter"); // Report Filter
// Initialize table with empty data
let table = $dataTable.DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
stateSave: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
drawCallback: function( settings ) {
setTimeout(function() {
$("#applyBtn").removeClass('disabled');
}, 300);
},
filter: false,
order: [[1, "asc"]],
data:{},
"columns": [
{"data": "labelDate"},
{"data": "adPlays"},
{
"data": "impressions",
"render": dataTableRoundDecimal
},
{
"data": "spend",
"render": dataTableRoundDecimal
}
],
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $("#stats").closest(".XiboGrid").find(".FilterDiv form").serializeObject(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
if (result.error) {
toastr.error(result.error);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
// Sort table by period
if (Object.keys(data).length > 0) {
table.rows.add(data).order([0, 'asc']).draw()
}
}
function setChartData(data) {
imageLoader.show();
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
imageLoader.hide();
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
if (reportFilter.val() === '' || reportFilter.val() === undefined) {
$(".from-dt").show();
$(".to-dt").show();
} else {
$(".from-dt").hide();
$(".to-dt").hide();
}
}
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#fromDt").val());
let toDt = moment($("#toDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#fromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#toDtLink").change( function() {
calculateDaysShowHideWarn();
});
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
let anchorReportAddBtn = $("button#reportAddBtn");
anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=displayAdPlay" );
};
imageLoader.hide();
checkReportFilter(reportFilter);
checkFilterAndApply();
var $campaignSelect = $('#parentCampaignId');
$campaignSelect.select2({
ajax: {
url: $campaignSelect.data("searchUrl"),
dataType: "json",
delay: 250,
placeholder: 'Campaign',
allowClear: true,
data: function(params) {
var query = {
isLayoutSpecific: 0,
retired: 0,
totalDuration: 0,
name: params.term,
start: 0,
length: 10,
columns: [
{
"data": "isLayoutSpecific"
},
{
"data": "campaign"
}
],
order: [
{
"column": 0,
"dir": "asc"
},
{
"column": 1,
"dir": "asc"
}
]
};
// Set the start parameter based on the page number
if (params.page != null) {
query.start = (params.page - 1) * 10;
}
return query;
},
processResults: function(data, params) {
var results = [];
var campaigns = [];
$.each(data.data, function(index, element) {
campaigns.push({
"id": element.campaignId,
"text": element.campaign
});
});
results.push({
"text": $campaignSelect.data('transCampaigns'),
"children": campaigns
})
var page = params.page || 1;
page = (page > 1) ? page - 1 : page;
return {
results: results,
pagination: {
more: (page * 10 < data.recordsTotal)
}
}
}
}
})
});
function reportScheduleCallback() {
let $displayId = $('#report #displayId');
let $newDisplayId = $('#reportScheduleAddForm #displayId');
appendOptions($newDisplayId, $displayId.find('option:selected').clone());
}
function appendOptions(element, options) {
for (let i = 0; i < options.length; i++) {
let option = options[i];
element.append(option).trigger('change');
$(option).prop('selected', true);
element.trigger({
type: 'select2:select',
params: {
data: option
}
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Ad Plays" %}</th>
<th>{% trans "Impressions" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
"data": outputData,
"columns": [
{
"data": "labelDate",
"render": dataTableDateFromIso
},
{"data": "adPlays"},
{"data": "impressions"}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}reportScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-default-values", value: displayId },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,704 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Display Played Percentage" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Display Played Percentage" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="displayPercentageView" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="displayPercentageReport">
<!-- Form Filter -->
<form class="form-inline">
{# Campaign list only. #}
{% set attributes = [
{ name: "data-search-url", value: url_for("campaign.search") },
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
] %}
{% set title %}{% trans "Campaign" %} * {% endset %}
{% set helpText %}{% trans "Please select a Campaign" %}{% endset %}
{{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="true">Tabular</a>
</li>
<li class="nav-item">
<a class="nav-link" id="spend-chart-tab" data-toggle="tab" href="#spendChartTab" role="tab"
aria-controls="chartTab" aria-selected="false">Chart (Spend)</a>
</li>
<li class="nav-item">
<a class="nav-link" id="playtime-chart-tab" data-toggle="tab" href="#playtimeChartTab" role="tab"
aria-controls="chartTab" aria-selected="false">Chart (Playtime)</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- TABULAR TAB-->
<div class="tab-pane active" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="displayPercentageTbl"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-state-preference-name="displayPercentageGrid"
data-url="/report/data/displayPercentage">
<thead>
<tr>
<th>{% trans "Display" %}</th>
<th>{% trans "Spend(%)" %}</th>
<th>{% trans "Playtime(%)" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- CHART TAB-->
<div class="tab-pane show" id="spendChartTab" role="tabpanel" aria-labelledby="spend-chart-tab">
<div id="otherSpend" style="position: absolute;">
<div class="switch-other-main">
<input type="checkbox" id="otherSpendSwitch" checked
data-chart-name="spendChart" data-chart-canvas="spendChartCanvas"
data-on-text="Main" data-off-text="Other"/>
</div>
</div>
<div class="chart-container" style="height:550px;">
<canvas id="spendChartCanvas"
{# style="clear:both; margin-top:25px;" height="70%"#}
></canvas>
</div>
</div>
<!-- CHART TAB-->
<div class="tab-pane show" id="playtimeChartTab" role="tabpanel" aria-labelledby="playtime-chart-tab">
<div id="otherPlaytime" style="position: absolute;">
<div class="switch-other-main">
<input type="checkbox" id="otherPlaytimeSwitch" checked
data-chart-name="playtimeChart" data-chart-canvas="playtimeChartCanvas"
data-on-text="Main" data-off-text="Other"/>
</div>
</div>
<div class="chart-container" style="height:550px;">
<canvas id="playtimeChartCanvas"
{# style="clear:both; margin-top:25px;" height="70%"#}
></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let $report = $("#displayPercentageReport");
let $dataTable = $('#displayPercentageTbl'); // Datatable
// Charts
let $spendChartCanvas = $('#spendChartCanvas');
let $playtimeChartCanvas = $('#playtimeChartCanvas');
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Initialize table with empty data
let table = $dataTable.DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
stateSave: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
filter: false,
order: [[1, "asc"]],
data:{},
"columns": [
{
data: 'label',
sortable: false
},
{
data: 'spendData',
sortable: false
},
{
data: 'playtimeDuration',
sortable: false
}
],
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $("#displayPercentageTbl").closest(".XiboGrid").find(".FilterDiv form").serializeObject(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
// Based on tab load data
let activeTabHref = $('.nav-tabs .nav-item a.active').attr("href");
if (activeTabHref === '#spendChartTab') {
setSpendChartData($spendChartCanvas, result.table);
} else if (activeTabHref === '#playtimeChartTab') {
setPlaytimeChartData($playtimeChartCanvas, result.table);
} else {
setTabularData(table, result.table);
}
if (result.error) {
toastr.error(result.error);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
// Sort table by display spend
table.rows.add(data).order([1, 'desc']).draw()
}
}
$('.switch-other-main').hide();
function drawChart(chartData, configOptions, $ctx) {
return new Chart($ctx, {
type: 'doughnut',
plugins: [ChartDataLabels],
data: chartData,
options: configOptions,
});
}
function updateChartData(chartName, labels, chartData, bgColor) {
eval(chartName).data.labels = labels;
eval(chartName).data.datasets[0].data = chartData;
eval(chartName).data.datasets[0].backgroundColor = bgColor;
eval(chartName).update();
}
function updateConfigAsNewObject(chartName, total) {
eval(chartName).options = {
plugins: {
legend: {
position: 'top'
},
datalabels: {
render: 'percentage',
fontColor: 'white',
precision: 2,
}
},
maintainAspectRatio: false,
animation: {
onComplete: function onComplete(animation) {
// hideLoading();
}
}
};
eval(chartName).update();
}
$("#otherSpendSwitch, #otherPlaytimeSwitch").bootstrapSwitch({
onSwitchChange: function(e, state) {
let chartName = $(this).attr("data-chart-name");
let mainTotal = $(this).attr("data-main-total");
// Main Chart Info
let mainChartData = $(this).attr("data-main-data").split(",");
let mainLabels = $(this).attr("data-main-labels").split(",");
let mainBgColor = $(this).attr("data-main-bg-color").split(",");
// Other Chart Info
let otherChartData = $(this).attr("data-other-data").split(",");
let otherLabels = $(this).attr("data-other-labels").split(",");
let otherBgColor = $(this).attr("data-other-bg-color").split(",");
if (state) {
updateChartData(chartName, mainLabels, mainChartData, mainBgColor);
} else {
updateChartData(chartName, otherLabels, otherChartData, otherBgColor);
}
// Update chart config when other/main button pressed
updateConfigAsNewObject(chartName, mainTotal);
}
});
// Set chart data
let spendChart;
function setSpendChartData($chartCanvas, response) {
$('.chart-container .alert-info').remove();
if (spendChart !== undefined && spendChart !== null) {
spendChart.destroy();
}
let mainChartTotal = 0;
$.each(response, function (index, element) {
// Get total
mainChartTotal += element.spendData;
});
// Convert our data into a dataset we can use for this chart.
let labels = [];
let data = [];
let backgroundColor = [];
let sum = 0;
let otherChartTotal = 0;
let otherChartData = [];
let otherChartLabels = [];
let otherChartBgColor = [];
$.each(response, function(index, element) {
let percent = 0;
percent = element.spendData / mainChartTotal * 100;
if (percent < 10) {
// Keep track of total of Other
otherChartTotal += element.spendData;
otherChartData.push(element.spendData);
otherChartLabels.push(element.label);
otherChartBgColor.push(element.backgroundColor);
} else {
labels.push(element.label);
data.push(element.spendData);
backgroundColor.push(element.backgroundColor);
}
sum += element.spendData;
});
if (sum <= 0) {
// Show a message
if ($('.chart-container .alert-info').length <= 0) {
$('.chart-container').append($('<div class="alert alert-info">No display data for Campaign.</div>'))
}
return;
}
let $otherSpendBtns = $('#otherSpend .switch-other-main');
if (otherChartData.length === 1) {
// We push only on other slice in main chart
labels.push(otherChartLabels);
data.push(otherChartData);
backgroundColor.push(otherChartBgColor);
} else if (otherChartData.length > 1) {
// We have other slice in chart
// Combine the Other slice in Main Chart
labels.push('Other');
data.push(otherChartTotal);
// Set background color of "Other Slice"
backgroundColor.push('#808080');
// Show two buttons Other and Main
$otherSpendBtns.show();
}
let chartData = {
labels: labels,
datasets: [
{
label: 'Display Played Percentage',
data: data,
backgroundColor: backgroundColor,
}],
};
let configOptions = {
plugins: {
legend: {
position: 'top',
},
datalabels: {
render: 'percentage',
fontColor: 'white',
precision: 2,
},
},
maintainAspectRatio: false,
animation: {
onComplete: function(animation) {
// hideLoading();
}
},
};
spendChart = drawChart(chartData, configOptions, $('#spendChartCanvas'));
// Update chart config
updateConfigAsNewObject('spendChart', mainChartTotal);
$otherSpendBtns.find("input").attr("data-other-labels", otherChartLabels);
$otherSpendBtns.find("input").attr("data-other-data", otherChartData);
$otherSpendBtns.find("input").attr("data-other-bg-color", otherChartBgColor);
$otherSpendBtns.find("input").attr("data-main-labels", labels);
$otherSpendBtns.find("input").attr("data-main-data", data);
$otherSpendBtns.find("input").attr("data-main-bg-color", backgroundColor);
$otherSpendBtns.find("input").attr("data-main-total", mainChartTotal);
}
let playtimeChart;
function setPlaytimeChartData($chartCanvas, response) {
$('.chart-container .alert-info').remove();
if (playtimeChart !== undefined && playtimeChart !== null) {
playtimeChart.destroy();
}
let mainChartTotal = 0;
$.each(response, function (index, element) {
// Get total
mainChartTotal += element.playtimeDuration;
});
// Convert our data into a dataset we can use for this chart.
let labels = [];
let data = [];
let backgroundColor = [];
let sum = 0;
let otherChartTotal = 0;
let otherChartData = [];
let otherChartLabels = [];
let otherChartBgColor = [];
$.each(response, function(index, element) {
let percent = 0;
percent = element.playtimeDuration / mainChartTotal * 100;
if (percent < 10) {
// Keep track of total of Other
otherChartTotal += element.playtimeDuration;
otherChartData.push(element.playtimeDuration);
otherChartLabels.push(element.label);
otherChartBgColor.push(element.backgroundColor);
} else {
labels.push(element.label);
data.push(element.playtimeDuration);
backgroundColor.push(element.backgroundColor);
}
sum += element.playtimeDuration;
});
if (sum <= 0) {
// Show a message
if ($('.chart-container .alert-info').length <= 0) {
$('.chart-container').append($('<div class="alert alert-info">No display data for Campaign.</div>'))
}
return;
}
let $otherPlaytimeBtns = $('#otherPlaytime .switch-other-main');
if (otherChartData.length === 1) {
// We push only on other slice in main chart
labels.push(otherChartLabels);
data.push(otherChartData);
backgroundColor.push(otherChartBgColor);
} else if (otherChartData.length > 1) {
// We have other slice in chart
// Combine the Other slice in Main Chart
labels.push('Other');
data.push(otherChartTotal);
backgroundColor.push('#808080');
// Show two buttons Other and Main
$otherPlaytimeBtns.show();
}
let chartData = {
labels: labels,
datasets: [
{
label: 'Display Played Percentage',
data: data,
backgroundColor: backgroundColor,
}],
};
let configOptions = {
plugins: {
legend: {
position: 'top',
},
datalabels: {
render: 'percentage',
fontColor: 'white',
precision: 2,
},
},
maintainAspectRatio: false,
animation: {
onComplete: function(animation) {
// hideLoading();
}
},
};
playtimeChart = drawChart(chartData, configOptions, $('#playtimeChartCanvas'));
// Update chart config
updateConfigAsNewObject('playtimeChart', mainChartTotal);
$otherPlaytimeBtns.find("input").attr("data-other-labels", otherChartLabels);
$otherPlaytimeBtns.find("input").attr("data-other-data", otherChartData);
$otherPlaytimeBtns.find("input").attr("data-other-bg-color", otherChartBgColor);
$otherPlaytimeBtns.find("input").attr("data-main-labels", labels);
$otherPlaytimeBtns.find("input").attr("data-main-data", data);
$otherPlaytimeBtns.find("input").attr("data-main-bg-color", backgroundColor);
$otherPlaytimeBtns.find("input").attr("data-main-total", mainChartTotal);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#spendChartTab') {
setSpendChartData($spendChartCanvas, result.table);
} else if (activeTab === '#playtimeChartTab') {
setPlaytimeChartData($playtimeChartCanvas, result.table);
} else {
setTabularData(table, result.table);
}
}
});
// todo
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
checkFilterAndApply();
getData($dataTable.data().url);
});
let checkFilterAndApply = function() {
let reportAddBtn = $("button#reportAddBtn");
let $parentCampaign = $('#parentCampaignId');
// If we select a displayId we hide the display group filter
$parentCampaign.off('change').change( function() {
let parentCampaignId = $(this).val();
if (parentCampaignId) {
$applyBtn.removeClass('disabled');
reportAddBtn.removeClass('disabled');
} else {
$("#parentCampaignId option").remove();
$applyBtn.addClass('disabled');
reportAddBtn.addClass('disabled');
}
});
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?parentCampaignId=" + $parentCampaign.val() + "&reportName=displayPercentage" );
};
imageLoader.hide();
$applyBtn.addClass('disabled');
checkFilterAndApply();
// Bind to form change
$report.on('change', function() {
checkFilterAndApply();
});
var $campaignSelect = $('#parentCampaignId');
$campaignSelect.select2({
ajax: {
url: $campaignSelect.data("searchUrl"),
dataType: "json",
delay: 250,
placeholder: 'Campaign',
allowClear: true,
data: function(params) {
var query = {
isLayoutSpecific: 0,
retired: 0,
totalDuration: 0,
name: params.term,
start: 0,
length: 10,
columns: [
{
"data": "isLayoutSpecific"
},
{
"data": "campaign"
}
],
order: [
{
"column": 0,
"dir": "asc"
},
{
"column": 1,
"dir": "asc"
}
]
};
// Set the start parameter based on the page number
if (params.page != null) {
query.start = (params.page - 1) * 10;
}
return query;
},
processResults: function(data, params) {
var results = [];
var campaigns = [];
$.each(data.data, function(index, element) {
campaigns.push({
"id": element.campaignId,
"text": element.campaign
});
});
results.push({
"text": $campaignSelect.data('transCampaigns'),
"children": campaigns
})
var page = params.page || 1;
page = (page > 1) ? page - 1 : page;
return {
results: results,
pagination: {
more: (page * 10 < data.recordsTotal)
}
}
}
}
})
});
function reportScheduleCallback() {
let $parentCampaignId = $('#report #parentCampaignId');
let $newParentCampaignId = $('#reportScheduleAddForm #parentCampaignId');
appendOptions($newParentCampaignId, $parentCampaignId.find('option:selected').clone());
}
function appendOptions(element, options) {
for (let i = 0; i < options.length; i++) {
let option = options[i];
element.append(option).trigger('change');
$(option).prop('selected', true);
element.trigger({
type: 'select2:select',
params: {
data: option
}
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Display" %}</th>
<th>{% trans "Spend(%)" %}</th>
<th>{% trans "Playtime(%)" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
"data": outputData,
"columns": [
{
data: 'label',
},
{
data: 'spendData',
},
{
data: 'playtimeDuration',
}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}reportScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Event Type" %}</th>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Reference ID" %}</th>
<th>{% trans "Detail" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.displayId }}</td>
<td>{{ item.display }}</td>
<td>{{ item.eventType }}</td>
<td>{{ item.start }}</td>
<td>{{ item.end }}</td>
<td>{{ item.refId }}</td>
<td>{{ item.detail }}</td>
</tr>
{% endfor %}
</table>
<br/>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
{% endblock %}

View File

@@ -0,0 +1,315 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Display Alerts" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Display Alerts" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom">
<div class="FilterDiv card-body" id="displayAlertsFilter">
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{{ inline.dateRangeFilter("reportFilter", title, "", "", "", "", "") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Event Type" %}{% endset %}
{% set options = [
{ id: -1, value: "" },
{ id: 1, value: "Display Up/down" },
{ id: 2, value: "App Start" },
{ id: 3, value: "Power Cycle" },
{ id: 4, value: "Network Cycle" },
{ id: 5, value: "TV Monitoring" },
{ id: 6, value: "Player Fault" },
{ id: 7, value: "Command" },
{ id: 8, value: "Other" }
] %}
{{ inline.dropdown("eventType", "single", title, -1, options, "id", "value") }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter --no-tag to see items without tags." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set title %}{% trans "Only show currently logged in?" %}{% endset %}
{{ inline.checkbox("onlyLoggedIn", title) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Body -->
<div class="card-body">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="displayAlertsGrid"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'displayalerts'}) }}">
<thead>
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Event Type" %}</th>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Detail" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
let $report = $("#displayAlertsFilter");
let $dataTable = $('#displayAlertsGrid'); // Datatable
let result; // XHR get data result
let reportData = '';
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Initialize table with empty data
let table = $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
order: [[3, 'desc']],
data: {},
columns: [
{data: 'displayId'},
{data: 'display'},
{data: 'eventType', "sortable": false},
{
name: 'start',
data: function(data) {
if(data.start) {
return moment(data.start, 'X').format(jsDateFormat)
} else {
return '';
}
}
},
{
name: 'end',
data: function(data) {
if(data.end) {
return moment(data.end, 'X').format(jsDateFormat)
} else {
return '';
}
}
},
{
name: 'duration',
data: function(data) {
if (data.start && data.end) {
let durationData = moment.duration(data.end - data.start, "seconds");
let dataM = '';
let months = durationData.months();
if (months > 0) {
durationData.subtract(moment.duration(months, 'months'));
dataM += months + '{% trans "month" %} ';
}
let days = durationData.days();
durationData.subtract(moment.duration(days, 'days'));
dataM += days + '{% trans "day" %} ';
let hours = durationData.hours();
durationData.subtract(moment.duration(hours, 'hours'));
dataM += hours + '{% trans "hr" %} ';
let minutes = durationData.minutes();
durationData.subtract(moment.duration(minutes, 'minutes'));
dataM += minutes + '{% trans "min" %} ';
let seconds = durationData.seconds();
dataM += seconds + '{% trans "sec" %} ';
return dataM;
}
return '';
}
},
{data: 'refId'},
{data: 'detail'}
]
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find("form").serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
setTabularData(table, result.table);
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
function displayAlertsReportScheduleFormOpen(dialog) {
// If we select a displayId we hide the display group filter
$('#reportScheduleAddForm #displayId').off('change').change( function() {
let displayId = $('#reportScheduleAddForm #displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().hide();
} else {
$('#reportScheduleAddForm #displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().show();
}
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,122 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="displayAlertsReportPreview" class="table table-striped">
<thead>
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Event Type" %}</th>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Reference" %}</th>
<th>{% trans "Detail" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
var outputData = {{ table|json_encode|raw }};
var table = $("#displayAlertsReportPreview").DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
paging: false,
ordering: false,
info: false,
order: [[3, 'desc']],
searching: false,
data: outputData,
columns: [
{data: 'displayId'},
{data: 'display'},
{data: 'eventType', "sortable": false},
{
name: 'start',
data: function(data) {
if(data.start) {
return moment(data.start, 'X').format(jsDateFormat)
} else {
return '';
}
}
},
{
name: 'end',
data: function(data) {
if(data.end) {
return moment(data.end, 'X').format(jsDateFormat)
} else {
return '';
}
}
},
{data: 'refId'},
{data: 'detail'}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,106 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}displayAlertsReportScheduleFormOpen{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "displayalerts",
"description": "Display Alerts",
"class": "\\Xibo\\Report\\DisplayAlerts",
"type": "Report",
"output_type": "table",
"color":"orange",
"fa_icon": "fa-bell",
"sort_order": 5,
"hidden": 0,
"category": "Display",
"feature": "displays.reporting",
"adminOnly": 0
}

View File

@@ -0,0 +1,48 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.label }}</td>
<td>{{ item.duration }}</td>
<td>{{ item.count }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,562 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2019 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Distribution by Layout, Media or Event" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Distribution by Layout, Media or Event" %}</span>
<span class="fa fa-info-circle widget-title-info px-1" data-toggle="popover" data-trigger="hover" data-placement="bottom" data-content="{% trans "This chart shows an aggregate duration and number of plays for the selected Layout, Media or Event. Please select your Range and Type below. Where the Range crosses period boundaries the data is aggregated into the correct period - i.e 1 week grouped by hourly produces 24 periods." %}"></span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="distributionReport">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Filter" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set byhour %}{% trans "Hour" %}{% endset %}
{% set bydayofweek %}{% trans "Day of week" %}{% endset %}
{% set bydayofmonth %}{% trans "Day of month" %}{% endset %}
{% set options = [
{ filterName: "byhour", groupByFilter: byhour },
{ filterName: "bydayofweek", groupByFilter: bydayofweek },
{ filterName: "bydayofmonth", groupByFilter: bydayofmonth },
] %}
{{ inline.dropdown("groupByFilter", "single", title, "", options, "filterName", "groupByFilter", "", "group-by-filter") }}
{% set title %}{% trans "Type" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set event %}{% trans "Event" %}{% endset %}
{% set options = [
{ typeid: "layout", type: layout },
{ typeid: "media", type: media },
{ typeid: "event", type: event },
] %}
{{ inline.dropdown("type", "single", title, "", options, "typeid", "type") }}
{% set title %}{% trans "Layout" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }}
{% set title %}{% trans "Media" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Media" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("library.search") },
{ name: "data-search-term", value: "media" },
{ name: "data-id-property", value: "mediaId" },
{ name: "data-text-property", value: "name" }
] %}
{{ inline.dropdown("mediaId", "single", title, "", null, "mediaId", "name", helpText, "pagedSelect media-select", "", "m", "", attributes) }}
{% set title %}{% trans "Tag" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Event" %}{% endset %}
{{ inline.input("eventTag", title, "", helpText, "tag-text") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
aria-controls="chartTab" aria-selected="true">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="false">Tabular</a>
</li>
</ul>
</div> <!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- CHART TAB-->
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
<div class="chart-container" style="height:550px;">
<canvas id="canvas" style="clear:both; margin-top:25px;" height="70%"></canvas>
</div>
</div>
<!-- TABULAR TAB-->
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="distributionTbl"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="/report/data/distributionReport"
>
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
let $report = $("#distributionReport");
let $dataTable = $('#distributionTbl'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
let type = $("#type");
let mediaSelect =$(".media-select");
let layoutSelect =$(".layout-select");
let eventTagCls =$(".tag-text");
let reportFilter = $("#reportFilter"); // Report Filter
// Initialize table with empty data
let table = $dataTable.DataTable({
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
ordering: false,
data: {},
columns: [
{
data: 'label',
'sortable': false,
},
{
data: 'duration',
'sortable': false,
},
{
data: 'count',
'sortable': false,
}
],
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Total over all pages
let totalDuration = api.column(1).data().map(Number).reduce((a, b) => a + b, 0);
let totalNumberPlays = api.column(2).data().map(Number).reduce((a, b) => a + b, 0);
let totalDurationPage = api.column(1, { page: 'current'}).data().map(Number)
.reduce((a, b) => a + b, 0);
let totalNumberPlaysPage = api.column(2, { page: 'current'}).data().map(Number)
.reduce((a, b) => a + b, 0);
// Update footer
$(api.column(1).footer())
.html(`${totalDurationPage} (${totalDuration} total)`);
$(api.column(2).footer())
.html(`${Math.floor(totalNumberPlaysPage)} (${Math.floor(totalNumberPlays)} total)`);
},
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find("form").serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
checkFilterAndApply();
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#statsFromDt").val());
let toDt = moment($("#statsToDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#statsFromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#statsToDtLink").change( function() {
calculateDaysShowHideWarn();
});
// Enable/Disable Schedule Btn
let checkEnableSchedule = function() {
// Schedule button enable/disable - start
let mediaVal = $("#mediaId").val();
let layoutVal = $("#layoutId").val();
let eventTagVal = $("#eventTag").val();
let reportAddBtn = $("button#reportAddBtn");
let typeVal = $("#type").val();
if ( typeVal === 'layout') {
if (layoutVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&layoutId=" + layoutVal + "&reportName=distributionReport" );
reportAddBtn.removeAttr('title');
}
} else if ( typeVal === 'media') {
if (mediaVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&mediaId=" + mediaVal + "&reportName=distributionReport" );
reportAddBtn.removeAttr('title');
}
} else if ( typeVal === 'event') {
if (eventTagVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&eventTag=" + eventTagVal + "&reportName=distributionReport" );
reportAddBtn.removeAttr('title');
}
}
};
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
if (reportFilter.val() === '' || reportFilter.val() === undefined) {
$(".stats-from-dt").show();
$(".stats-to-dt").show();
} else {
$(".stats-from-dt").hide();
$(".stats-to-dt").hide();
}
}
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
type.off('change').change( function() {
let value = type.val();
if (value === 'media') {
// show media and clear/hide the layout
$("#layoutId").val("");
$("#layoutId option").remove();
layoutSelect.hide();
$("#eventTag").val("");
eventTagCls.hide();
mediaSelect.show();
} else if (value === 'layout') {
// show layout and clear/hide the media
$("#mediaId").val("");
$("#mediaId option").remove();
mediaSelect.hide();
$("#eventTag").val("");
eventTagCls.hide();
layoutSelect.show();
} else if (value === 'event') {
// clear/hide the media and layout
$("#mediaId").val("");
$("#mediaId option").remove();
$("#layoutId").val("");
$("#layoutId option").remove();
mediaSelect.hide();
layoutSelect.hide();
// show tag
eventTagCls.show();
}
});
};
type.val('layout');
mediaSelect.hide();
eventTagCls.hide();
checkReportFilter(reportFilter);
checkFilterAndApply();
$applyBtn.addClass('disabled');
checkEnableSchedule();
// Bind to form change
$report.on('change', function() {
checkEnableSchedule();
let layoutVal = $("#layoutId").val();
let mediaVal = $("#mediaId").val();
let eventVal = $("#eventTag").val();
if ((layoutVal === null || layoutVal === '' || layoutVal === undefined) &&
(mediaVal === null || mediaVal === '' || mediaVal === undefined) &&
(eventVal === null || eventVal === '' || eventVal === undefined) ) {
$applyBtn.addClass('disabled');
} else {
$applyBtn.removeClass('disabled');
}
});
});
function distributionScheduleCallback(dialog) {
// If we select a displayId we hide the display group filter
$('#reportScheduleAddForm #displayId').off('change').change( function() {
let displayId = $('#reportScheduleAddForm #displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().hide();
} else {
$('#reportScheduleAddForm #displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().show();
}
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,96 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2019 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<canvas id="canvas" style="clear:both; margin-top:25px"></canvas>
</div>
<br/>
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let reportChart = new Chart($("#canvas"), {{ chart|json_encode|raw }});
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"searching": false,
"paging": true,
"ordering": false,
"data": outputData,
"columns": [
{ "data": 'label' },
{ "data": 'duration' },
{ "data": 'count' },
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,119 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{{ formTitle }}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}distributionScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set byhour %}{% trans "Hour" %}{% endset %}
{% set bydayofweek %}{% trans "Day of week" %}{% endset %}
{% set bydayofmonth %}{% trans "Day of month" %}{% endset %}
{% set options = [
{ filterName: "byhour", groupByFilter: byhour },
{ filterName: "bydayofweek", groupByFilter: bydayofweek },
{ filterName: "bydayofmonth", groupByFilter: bydayofmonth },
] %}
{{ forms.dropdown("groupByFilter", "single", title, "", options, "filterName", "groupByFilter", "", "group-by-filter") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "distributionReport",
"description": "Chart: Distribution by Layout, Media or Event",
"class": "\\Xibo\\Report\\DistributionReport",
"type": "Chart",
"output_type": "both",
"color":"green",
"fa_icon": "fa-bar-chart",
"sort_order": 3,
"hidden": 0,
"category": "Proof of Play",
"feature": "proof-of-play",
"adminOnly": 0
}

View File

@@ -0,0 +1,53 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<table class="saved-report-table">
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Usage" %}</th>
<th>{% trans "Count Files" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.userId }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.bytesUsedFormatted }}</td>
<td>{{ item.numFiles }}</td>
</tr>
{% endfor %}
</table>
{% for key,item in multipleCharts %}
<div style="text-align: center">{{ key|replace({'_': " "}) }}</div>
<img src="{{ item|raw }}" >
<p></p>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,231 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Library Usage" %} | {% endblock %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-success XiboFormButton" title="Schedule" id="reportAddBtn" href="{{ url_for("reportschedule.add.form") }}?reportName=libraryusage"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Schedule" %}</button>
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}?reportName=libraryusage"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
<button class="btn btn-primary XiboRedirectButton" href="{{ url_for("reportschedule.view") }}?reportName=libraryusage"><i class="fa fa-th-list" aria-hidden="true"></i> {% trans "Report Schedules" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Library Usage" %}</span>
</div>
<div class="widget-navigation-menu">
<ul class="nav nav-pills">
<li role="presentation" class="nav-item"><a class="nav-link" href="{{ url_for("report.view") }}">{% trans "All Reports" %}</a></li>
<li role="presentation" class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> Reports <span class="caret"></span> </a>
<div class="dropdown-menu">
{% for reports in defaults.availableReports %}
{% for report in reports %}
{% if report.hidden == 0 and report.category == 'Library'%}
<a class="dropdown-item" href="{{ url_for("report.form", {name: report.name}) }}">{{ report.description }}</a>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</li>
</ul>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilter card mb-3 bg-light">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set attributes = [
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" }
] %}
{% set title %}{% trans "User" %}{% endset %}
{% set userFilterOptions = [{userId: null, user: ""}]|merge(defaults.users) %}
{{ inline.dropdown("userId", "single", title, "", userFilterOptions, "userId", "userName", "", "selectPicker", "", "u", "", attributes) }}
{% set title %}{% trans "User Group" %}{% endset %}
{% set groupFilterOptions = [{groupId: null, group: ""}]|merge(defaults.groups) %}
{{ inline.dropdown("groupId", "single", title, "", groupFilterOptions, "groupId", "group", "", "selectPicker", "", "g", "", attributes) }}
</form>
</div>
</div>
<div class="XiboData card pt-3">
<table id="libraryUsage" class="table table-striped">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Usage" %}</th>
<th>{% trans "Count Files" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="widget">
<div class="widget-title">
<i class="fa fa-tasks"></i>
{% if libraryLimitSet != "" %}
{% trans %}Library Usage. Limit {{ libraryLimit }}{% endtrans %}
{% else %}
{% trans "Library Usage" %}
{% endif %}
<div class="clearfix"></div>
</div>
<div class="widget-body medium no-padding">
<canvas id="libraryChart" style="clear:both;" width="350" height="220"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="widget">
<div class="widget-title">
<i class="fa fa-user"></i>
{% trans "User Percentage Usage" %}
<div class="clearfix"></div>
</div>
<div class="widget-body medium no-padding">
<canvas id="userChart" style="clear:both;" width="350" height="220"></canvas>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var userChart = null;
var table = $("#libraryUsage").DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
serverSide: true,
stateSave: true,
stateDuration: 0,
filter: false,
searchDelay: 3000,
ajax: {
url: "{{ url_for("report.data", {name: reportName}) }}",
data: function (d) {
$.extend(d, $("#libraryUsage").closest(".XiboGrid").find(".FilterDiv form").serializeObject());
},
dataFilter: function(data){
let json = $.parseJSON(data);
json.recordsFiltered = json.recordsTotal;
json.data = json.table;
return JSON.stringify( json ); // return JSON string
}
},
"columns": [
{ data: "userId" },
{ data: "userName" },
{ data: "bytesUsedFormatted" },
{ data: "numFiles" }
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
if (!processing) {
// Render a pie chart
if (userChart !== undefined && userChart !== null) {
userChart.destroy();
}
// Organise our rows into datasets for the chart
var totalSize = 0;
var userData = [];
var userLabels = [];
$.each(table.data(), function(index, el) {
totalSize += el.bytesUsed;
});
$.each(table.data(), function(index, el) {
userData.push(((el.bytesUsed/totalSize)*100).toFixed(2));
userLabels.push(el.userName);
});
var colours = [];
for(var i = 0; i < userData.length; i++) {
colours.push($c.rand());
}
// Create our chart
userChart = new Chart($("#userChart"), {
type: 'pie',
data: {
datasets: [{
data: userData,
backgroundColor: colours
}],
labels: userLabels
},
options: {
maintainAspectRatio: false
}
});
}
});
// Create a lovely library pie chart
var libraryData = {{ defaults.libraryWidgetData|raw }};
var colours = [];
for(var i = 0; i < libraryData.length; i++) {
colours.push($c.rand());
}
var libraryChart = new Chart($("#libraryChart"), {
type: 'pie',
data: {
datasets: [{
data: libraryData,
backgroundColor: colours
}],
labels: {{ defaults.libraryWidgetLabels|raw }}
},
options: {
maintainAspectRatio: false
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,133 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="libraryUsage" class="table table-striped">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Usage" %}</th>
<th>{% trans "Count Files" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="widget">
<div class="widget-title">
<i class="fa fa-tasks"></i>
{% if libraryLimitSet != "" %}
{% trans %}Library Usage. Limit {{ libraryLimit }}{% endtrans %}
{% else %}
{% trans "Library Usage" %}
{% endif %}
<div class="clearfix"></div>
</div>
<div class="widget-body medium no-padding">
<canvas id="libraryChart" style="clear:both;" width="350" height="220"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="widget">
<div class="widget-title">
<i class="fa fa-user"></i>
{% trans "User Percentage Usage" %}
<div class="clearfix"></div>
</div>
<div class="widget-body medium no-padding">
<canvas id="userChart" style="clear:both;" width="350" height="220"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
var outputData = {{ table|json_encode|raw }};
// Grid
var table = $("#libraryUsage").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
data: outputData,
columns: [
{ data: "userId" },
{ data: "userName" },
{ data: "bytesUsedFormatted" },
{ data: "numFiles" }
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
// User Percentage Usage
var userChart = new Chart($("#userChart"), {{ chart.User_Percentage_Usage|json_encode|raw }});
// Library Usage
var libraryChart = new Chart($("#libraryChart"), {{ chart.Library_Usage|json_encode|raw }});
</script>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set attributes = [
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" }
] %}
{% set title %}{% trans "User" %}{% endset %}
{% set userFilterOptions = [{userId: null, user: ""}]|merge(users) %}
{{ forms.dropdown("userId", "single", title, "", userFilterOptions, "userId", "userName", "", "selectPicker", "", "u", "", attributes) }}
{% set title %}{% trans "User Group" %}{% endset %}
{% set groupFilterOptions = [{groupId: null, group: ""}]|merge(groups) %}
{{ forms.dropdown("groupId", "single", title, "", groupFilterOptions, "groupId", "group", "", "selectPicker", "", "g", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "libraryusage",
"description": "Library Usage",
"class": "\\Xibo\\Report\\LibraryUsage",
"type": "Report",
"output_type": "both",
"color":"green",
"fa_icon": "fa-th",
"sort_order": 3,
"hidden": 0,
"category": "Library",
"feature": "admin",
"adminOnly": 1
}

View File

@@ -0,0 +1,62 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Display Id" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Layout Id" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Start Latitude" %}</th>
<th>{% trans "Start Longitude" %}</th>
<th>{% trans "End Latitude" %}</th>
<th>{% trans "End Longitude" %}</th>
<th>{% trans "Duration" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.from }}</td>
<td>{{ item.to }}</td>
<td>{{ item.displayId }}</td>
<td>{{ item.display }}</td>
<td>{{ item.layoutId }}</td>
<td>{{ item.layout }}</td>
<td>{{ item.startLat }}</td>
<td>{{ item.startLong }}</td>
<td>{{ item.endLat }}</td>
<td>{{ item.endLong }}</td>
<td>{{ item.duration }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,514 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Mobile Proof of Play" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Mobile Proof of Play" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="mobileProofOfPlayView" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="report">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set wholecampaign %}{% trans "Whole Campaign" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "wholecampaign", reportFilter: wholecampaign },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("fromDt", title, defaults.fromDateOneDay, "", "from-dt", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("toDt", title, defaults.toDate, "", "to-dt", "", "") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{# Campaign list only. #}
{% set attributes = [
{ name: "data-search-url", value: url_for("campaign.search") },
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
] %}
{% set title %}{% trans "Campaign" %}{% endset %}
{% set helpText %}{% trans "Please select a Campaign" %}{% endset %}
{{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }}
{% set title %}{% trans "Layout" %}{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="true">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- TABULAR TAB-->
<div class="tab-pane active" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="stats"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-state-preference-name="proofOfPlayGrid"
data-url="/report/data/mobileProofOfPlay">
<thead>
<tr>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Display Id" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Layout Id" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Start Latitude" %}</th>
<th>{% trans "Start Longitude" %}</th>
<th>{% trans "End Latitude" %}</th>
<th>{% trans "End Longitude" %}</th>
<th>{% trans "Duration" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let $dataTable = $('#stats'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Report Filter
let reportFilter = $("#reportFilter"); // Report Filter
// Grid
let table = $dataTable.DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
stateSave: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
drawCallback: function( settings ) {
setTimeout(function() {
$("#applyBtn").removeClass('disabled');
}, 300);
},
filter: false,
"order": [[0, "asc"]],
data:{},
"columns": [
{
"data": "from",
"render": dataTableDateFromIso
},
{
"data": "to",
"render": dataTableDateFromIso
},
{"data": "displayId"},
{"data": "display"},
{"data": "layoutId"},
{"data": "layout"},
{"data": "startLat"},
{"data": "startLong"},
{"data": "endLat"},
{"data": "endLong"},
{"data": "duration"}
],
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $("#stats").closest(".XiboGrid").find(".FilterDiv form").serializeObject(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
if (result.error) {
toastr.error(result.error);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
imageLoader.show();
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
imageLoader.hide();
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
if (reportFilter.val() === '' || reportFilter.val() === undefined) {
$(".from-dt").show();
$(".to-dt").show();
} else {
$(".from-dt").hide();
$(".to-dt").hide();
}
}
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#fromDt").val());
let toDt = moment($("#toDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#fromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#toDtLink").change( function() {
calculateDaysShowHideWarn();
});
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
let anchorReportAddBtn = $("button#reportAddBtn");
let type = $("#type").val();
let tagsType = $("#tagsType").val();
let tags = $("#tags").val();
let exactTags = $('#exactTags').is(":checked");
anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=mobileProofOfPlay" );
};
checkReportFilter(reportFilter);
checkFilterAndApply();
var $campaignSelect = $('#parentCampaignId');
$campaignSelect.select2({
ajax: {
url: $campaignSelect.data("searchUrl"),
dataType: "json",
delay: 250,
placeholder: 'Campaign',
allowClear: true,
data: function(params) {
var query = {
isLayoutSpecific: 0,
retired: 0,
totalDuration: 0,
name: params.term,
start: 0,
length: 10,
columns: [
{
"data": "isLayoutSpecific"
},
{
"data": "campaign"
}
],
order: [
{
"column": 0,
"dir": "asc"
},
{
"column": 1,
"dir": "asc"
}
]
};
// Set the start parameter based on the page number
if (params.page != null) {
query.start = (params.page - 1) * 10;
}
return query;
},
processResults: function(data, params) {
var results = [];
var campaigns = [];
$.each(data.data, function(index, element) {
campaigns.push({
"id": element.campaignId,
"text": element.campaign
});
});
results.push({
"text": $campaignSelect.data('transCampaigns'),
"children": campaigns
})
var page = params.page || 1;
page = (page > 1) ? page - 1 : page;
return {
results: results,
pagination: {
more: (page * 10 < data.recordsTotal)
}
}
}
}
})
});
function reportScheduleCallback() {
let $displayId = $('#report #displayId');
let $newDisplayId = $('#reportScheduleAddForm #displayId');
appendOptions($newDisplayId, $displayId.find('option:selected').clone());
}
function appendOptions(element, options) {
for (let i = 0; i < options.length; i++) {
let option = options[i];
element.append(option).trigger('change');
$(option).prop('selected', true);
element.trigger({
type: 'select2:select',
params: {
data: option
}
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,117 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Start" %}</th>
<th>{% trans "End" %}</th>
<th>{% trans "Display Id" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Layout Id" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Start Latitude" %}</th>
<th>{% trans "Start Longitude" %}</th>
<th>{% trans "End Latitude" %}</th>
<th>{% trans "End Longitude" %}</th>
<th>{% trans "Duration" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
"data": outputData,
"columns": [
{
"data": "from",
"render": dataTableDateFromIso
},
{
"data": "to",
"render": dataTableDateFromIso
},
{"data": "displayId"},
{"data": "display"},
{"data": "layoutId"},
{"data": "layout"},
{"data": "startLat"},
{"data": "startLong"},
{"data": "endLat"},
{"data": "endLong"},
{"data": "duration"}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}reportScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-default-values", value: displayId },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Type" %}</th>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Campaign" %}</th>
<th>{% trans "Layout ID" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Widget ID" %}</th>
<th>{% trans "Media" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Number of Plays" %}</th>
<th>{% trans "Total Duration (s)" %}</th>
<th>{% trans "First Shown" %}</th>
<th>{% trans "Last Shown" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.type }}</td>
<td>{{ item.displayId }}</td>
<td>{{ item.display }}</td>
<td>{{ item.parentCampaign }}</td>
<td>{{ item.layoutId }}</td>
<td>{{ item.layout }}</td>
<td>{{ item.widgetId }}</td>
<td>{{ item.media }}</td>
<td>{{ item.tag }}</td>
<td>{{ item.numberPlays }}</td>
<td>{{ item.duration }}</td>
<td>{{ item.minStart }}</td>
<td>{{ item.maxEnd }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,691 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2021 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Proof of Play" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Proof of Play" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-grid-name="proofOfPlayView" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="proofofplayReport">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }}
{% set title %}{% trans "Time" %}{% endset %}
{{ inline.time("statsFromDtTime", title, "00:00", "", "stats-from-dt-time") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }}
{% set title %}{% trans "Time" %}{% endset %}
{{ inline.time("statsToDtTime", title, "00:00", "", "stats-to-dt-time") }}
{% set title %}{% trans "Group By" %}{% endset %}
{% set options = [
{ id: "display", name: "Display" },
{ id: "displayGroup", name: "Display Group"|trans },
{ id: "tag", name: "Tag"|trans }
] %}
{{ inline.dropdown("groupBy", "single", title, "", options, "id", "name", "") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{% set title %}{% trans "Layout" %}{% endset %}
{{ inline.dropdown("layoutId[]", "dropdownmulti", title, "", null, "layoutId", "layout", "", "pagedSelect", "", "l", "", attributes) }}
{# Campaign list only. #}
{% set attributes = [
{ name: "data-search-url", value: url_for("campaign.search") },
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
] %}
{% set title %}{% trans "Campaign" %}{% endset %}
{% set helpText %}{% trans "Please select a Campaign" %}{% endset %}
{{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("library.search") },
{ name: "data-search-term", value: "media" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "mediaId" },
{ name: "data-text-property", value: "name" }
] %}
{% set title %}{% trans "Media" %}{% endset %}
{{ inline.dropdown("mediaId[]", "dropdownmulti", title, "", null, "mediaId", "name", "", "pagedSelect", "", "m", "", attributes) }}
{% set title %}{% trans "Type" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set widget %}{% trans "Widget" %}{% endset %}
{% set event %}{% trans "Event" %}{% endset %}
{% set options = [
{ typeid: "", type: null },
{ typeid: "layout", type: layout },
{ typeid: "media", type: media },
{ typeid: "widget", type: widget },
{ typeid: "event", type: event }
] %}
{{ inline.dropdown("type", "single", title, "", options, "typeid", "type") }}
{% set title %}{% trans "Tags from" %}{% endset %}
{% set dg %}{% trans "Display" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set options = [
{ tagsTypeid: "dg", tagsType: dg },
{ tagsTypeid: "layout", tagsType: layout },
{ tagsTypeid: "media", tagsType: media }
] %}
{{ inline.dropdown("tagsType", "single", title, "dg", options, "tagsTypeid", "tagsType") }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactMatchTitle %}{% trans "Should Tags filter by exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactMatchTitle, logicalOperatorTitle) }}
{% endif %}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="true">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- TABULAR TAB-->
<div class="tab-pane active" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="stats"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-state-preference-name="proofOfPlayGrid"
data-url="/report/data/proofofplayReport">
<thead>
<tr>
<th>{% trans "Type" %}</th>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Display Group ID" %}</th>
<th>{% trans "Display Group" %}</th>
<th>{% trans "Tag ID" %}</th>
<th>{% trans "Tag Name" %}</th>
<th>{% trans "Campaign" %}</th>
<th>{% trans "Layout ID" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Widget ID" %}</th>
<th>{% trans "Media" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Number of Plays" %}</th>
<th>{% trans "Total Duration" %}</th>
<th>{% trans "Total Duration (s)" %}</th>
<th title="{{ "NB: proof of play records which span your range are returned in this report."|trans }}">{% trans "First Period Shown" %}</th>
<th title="{{ "NB: proof of play records which span your range are returned in this report."|trans }}">{% trans "Last Period Shown" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let $report = $("#proofofplayReport");
let $dataTable = $('#stats'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Report Filter
let reportFilter = $("#reportFilter"); // Report Filter
// Grid
let table = $dataTable.DataTable({
"language": dataTablesLanguage,
dom: dataTablesTemplate,
stateSave: true,
stateDuration: 0,
stateLoadCallback: dataTableStateLoadCallback,
stateSaveCallback: dataTableStateSaveCallback,
drawCallback: function( settings ) {
setTimeout(function() {
$("#applyBtn").removeClass('disabled');
}, 300);
// We hide empty columns and display appropriate columns (ie ID and name)
switch ($('select[name="groupBy"]').val()) {
case 'displayGroup':
$(this.api().columns([1, 2, 5, 6]).visible(false));
$(this.api().columns([3, 4]).visible(true));
break;
case 'tag':
$(this.api().columns([1,2, 3, 4]).visible(false));
$(this.api().columns([5, 6]).visible(true));
break;
default:
$(this.api().columns([3, 4, 5, 6]).visible(false));
$(this.api().columns([1, 2]).visible(true));
}
},
filter: false,
"order": [[1, "asc"]],
data:{},
"columns": [
{"data": "type"},
{"data": "displayId"},
{"data": "display"},
{"data": "displayGroupId"},
{"data": "displayGroup"},
{"data": "tagId"},
{"data": "tagName"},
{"data": "parentCampaign"},
{"data": "layoutId"},
{"data": "layout"},
{"data": "widgetId"},
{"data": "media"},
{"data": "tag"},
{"data": "numberPlays"},
{
"data": "duration",
"render": function (data, type, row, meta) {
if (type != "display")
return "";
var durationData = moment.duration(data, "seconds");
var dataM = '';
var months = durationData.months();
if (months > 0) {
durationData.subtract(moment.duration(months,'months'));
dataM += months + '{% trans "month" %} ';
}
var days = durationData.days();
durationData.subtract(moment.duration(days,'days'));
dataM += days + '{% trans "day" %} ';
var hours = durationData.hours();
durationData.subtract(moment.duration(hours,'hours'));
dataM += hours + '{% trans "hr" %} ';
var minutes = durationData.minutes();
durationData.subtract(moment.duration(minutes,'minutes'));
dataM += minutes + '{% trans "min" %} ';
var seconds = durationData.seconds();
dataM += seconds + '{% trans "sec" %} ';
return dataM;
}
},
{"data": "duration"},
{"data": "minStart"},
{"data": "maxEnd"}
],
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Total over all pages
let totalNumberPlays = api.column(13).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalDuration = api.column(15).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalNumberPlaysPage = api.column(13, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
let totalDurationPage = api.column(15, { page: 'current'}).data().reduce(function (a, b) {
return a + b;
}, 0);
// Update footer
$(api.column(13).footer()).html(totalNumberPlaysPage + ' (' + totalNumberPlays + ' total)');
$(api.column(15).footer()).html(Math.floor(totalDurationPage) + ' (' + Math.floor(totalDuration) + ' total)');
},
});
// table.on('draw', dataTableDraw);
// table.on('processing.dt', dataTableProcessing);
// dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $("#stats").closest(".XiboGrid").find(".FilterDiv form").serializeObject(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons'));
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($dataTable.data().url);
});
// If we select a displayId, we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
$('select[name="groupBy[]"] option').remove();
$('select[name="groupBy"]').parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
$('select[name="groupBy"]').parent().show();
}
});
// If we select a groupBy data, we hide the display filter
$("select[name='groupBy']").on('change', function() {
let optionSelected = $(this).find("option:selected").val();
if (optionSelected === 'displayGroup') {
$('select[name="groupBy"]').parent().show();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
} else {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
}
if (optionSelected === 'display') {
$("select[name='displayId']").parent().show();
} else {
$("select[name='displayId']").parent().hide();
}
});
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
if (reportFilter.val() === '' || reportFilter.val() === undefined) {
$(".stats-from-dt").show();
$(".stats-to-dt").show();
$(".stats-from-dt-time").show();
$(".stats-to-dt-time").show();
} else {
$(".stats-from-dt").hide();
$(".stats-to-dt").hide();
$(".stats-from-dt-time").hide();
$(".stats-to-dt-time").hide();
}
}
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#statsFromDt").val());
let toDt = moment($("#statsToDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#statsFromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#statsToDtLink").change( function() {
calculateDaysShowHideWarn();
});
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
let anchorReportAddBtn = $("button#reportAddBtn");
let type = $("#type").val();
let tagsType = $("#tagsType").val();
let tags = $("#tags").val();
let exactTags = $('#exactTags').is(":checked");
anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + type + "&tagsType=" + tagsType + "&tags=" + tags + "&exactTags=" + exactTags
+ "&reportName=proofofplayReport" );
};
checkReportFilter(reportFilter);
checkFilterAndApply();
var $campaignSelect = $('#parentCampaignId');
$campaignSelect.select2({
ajax: {
url: $campaignSelect.data("searchUrl"),
dataType: "json",
delay: 250,
placeholder: 'Campaign',
allowClear: true,
data: function(params) {
var query = {
isLayoutSpecific: 0,
retired: 0,
totalDuration: 0,
name: params.term,
start: 0,
length: 10,
columns: [
{
"data": "isLayoutSpecific"
},
{
"data": "campaign"
}
],
order: [
{
"column": 0,
"dir": "asc"
},
{
"column": 1,
"dir": "asc"
}
]
};
// Set the start parameter based on the page number
if (params.page != null) {
query.start = (params.page - 1) * 10;
}
return query;
},
processResults: function(data, params) {
var results = [];
var campaigns = [];
$.each(data.data, function(index, element) {
campaigns.push({
"id": element.campaignId,
"text": element.campaign
});
});
results.push({
"text": $campaignSelect.data('transCampaigns'),
"children": campaigns
})
var page = params.page || 1;
page = (page > 1) ? page - 1 : page;
return {
results: results,
pagination: {
more: (page * 10 < data.recordsTotal)
}
}
}
}
})
});
function proofOfPlayScheduleCallback() {
let $displayId = $('#proofofplayReport #displayId');
let $layoutId = $('#proofofplayReport [id="layoutId[]"]');
let $mediaId = $('#proofofplayReport [id="mediaId[]"]');
let $newDisplayId = $('#proofofplayScheduleAddForm #displayId');
let $newLayoutId = $('#proofofplayScheduleAddForm [id="layoutId[]"]');
let $newMediaId = $('#proofofplayScheduleAddForm [id="mediaId[]"]');
appendOptions($newDisplayId, $displayId.find('option:selected').clone());
appendOptions($newLayoutId, $layoutId.find('option:selected').clone());
appendOptions($newMediaId, $mediaId.find('option:selected').clone());
}
function appendOptions(element, options) {
for (let i = 0; i < options.length; i++) {
let option = options[i];
element.append(option).trigger('change');
$(option).prop('selected', true);
element.trigger({
type: 'select2:select',
params: {
data: option
}
});
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,149 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2019 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Type" %}</th>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Campaign" %}</th>
<th>{% trans "Layout ID" %}</th>
<th>{% trans "Layout" %}</th>
<th>{% trans "Widget ID" %}</th>
<th>{% trans "Media" %}</th>
<th>{% trans "Tag" %}</th>
<th>{% trans "Number of Plays" %}</th>
<th>{% trans "Total Duration" %}</th>
<th>{% trans "Total Duration (s)" %}</th>
<th>{% trans "First Shown" %}</th>
<th>{% trans "Last Shown" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
"data": outputData,
"columns": [
{ "data": 'type' },
{ "data": 'displayId' },
{ "data": 'display' },
{"data": "parentCampaign"},
{ "data": 'layoutId' },
{ "data": 'layout' },
{ "data": 'widgetId' },
{ "data": 'media' },
{ "data": 'tag' },
{ "data": 'numberPlays' },
{
"data": "duration",
"render": function (data, type, row, meta) {
if (type !== "display")
return "";
let durationData = moment.duration(data, "seconds");
let dataM = '';
let months = durationData.months();
if (months > 0) {
durationData.subtract(moment.duration(months,'months'));
dataM += months + '{% trans "month" %} ';
}
let days = durationData.days();
durationData.subtract(moment.duration(days,'days'));
dataM += days + '{% trans "day" %} ';
let hours = durationData.hours();
durationData.subtract(moment.duration(hours,'hours'));
dataM += hours + '{% trans "hr" %} ';
let minutes = durationData.minutes();
durationData.subtract(moment.duration(minutes,'minutes'));
dataM += minutes + '{% trans "min" %} ';
let seconds = durationData.seconds();
dataM += seconds + '{% trans "sec" %} ';
return dataM;
}
},
{ "data": 'duration' },
{ "data": 'minStart' },
{ "data": 'maxEnd' },
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,189 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#proofofplayScheduleAddForm").submit()
{% endblock %}
{% block callBack %}proofOfPlayScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="proofofplayScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-default-values", value: displayId },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{% set title %}{% trans "Layout" %}{% endset %}
{{ forms.dropdown("layoutId[]", "dropdownmulti", title, "", null, "layoutId", "layout", "", "pagedSelect", "", "l", "", attributes) }}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("library.search") },
{ name: "data-search-term", value: "media" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "mediaId" },
{ name: "data-text-property", value: "name" }
] %}
{% set title %}{% trans "Media" %}{% endset %}
{{ forms.dropdown("mediaId[]", "dropdownmulti", title, "", null, "mediaId", "name", "", "pagedSelect", "", "m", "", attributes) }}
{% set title %}{% trans "Type" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set widget %}{% trans "Widget" %}{% endset %}
{% set event %}{% trans "Event" %}{% endset %}
{% set options = [
{ typeid: "", type: null },
{ typeid: "layout", type: layout },
{ typeid: "media", type: media },
{ typeid: "widget", type: widget },
{ typeid: "event", type: event }
] %}
{{ forms.dropdown("type", "single", title, type, options, "typeid", "type") }}
{% set title %}{% trans "Sort by" %}{% endset %}
{% set widgetId %}{% trans "WidgetId" %}{% endset %}
{% set type %}{% trans "Type" %}{% endset %}
{% set display %}{% trans "Display" %}{% endset %}
{% set displayId %}{% trans "Display ID" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set layoutId %}{% trans "Layout ID" %}{% endset %}
{% set tag %}{% trans "Tag" %}{% endset %}
{% set options = [
{ sortbyid: "widgetId", sortby: widgetId },
{ sortbyid: "type", sortby: type },
{ sortbyid: "display", sortby: display },
{ sortbyid: "displayId", sortby: displayId },
{ sortbyid: "media", sortby: media },
{ sortbyid: "layout", sortby: layout },
{ sortbyid: "layoutId", sortby: layoutId },
{ sortbyid: "tag", sortby: tag }
] %}
{{ forms.dropdown("sortBy", "single", title, "", options, "sortbyid", "sortby") }}
{% set title %}{% trans "Tags from" %}{% endset %}
{% set dg %}{% trans "Display Group" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set options = [
{ tagsTypeid: "dg", tagsType: dg },
{ tagsTypeid: "layout", tagsType: layout },
{ tagsTypeid: "media", tagsType: media }
] %}
{{ forms.dropdown("tagsType", "single", title, tagsType, options, "tagsTypeid", "tagsType") }}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Should Tags filter by exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter a tag|tag value to filter tags with values. Enter --no-tag to filter all items without tags. Enter - before a tag or tag value to exclude from results." %}{% endset %}
{{ forms.inputWithTags("tags", title, tags, helpText, "", "", "", "exactTag", exactTagTitle, logicalOperatorTitle, 0, 'OR') }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "proofofplayReport",
"description": "Proof of Play",
"class": "\\Xibo\\Report\\ProofOfPlay",
"type": "Report",
"output_type": "table",
"color":"blue",
"fa_icon": "fa-th",
"sort_order": 1,
"hidden": 0,
"category": "Proof of Play",
"feature": "proof-of-play",
"adminOnly": 0
}

View File

@@ -0,0 +1,56 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Session ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Object" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.logDate }}</td>
<td>{{ item.userName }}</td>
<td>{{ item.userId }}</td>
<td>{{ item.ipAddress }}</td>
<td>{{ item.sessionHistoryId }}</td>
<td>{{ item.message }}</td>
<td>{{ item.objectAfter }}</td>
</tr>
{% endfor %}
</table>
<br/>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
{% endblock %}

View File

@@ -0,0 +1,411 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Session History" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Session History" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom">
<div class="FilterDiv card-body" id="sessionHistoryFilter">
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{{ inline.dateRangeFilter("reportFilter", title, "", "", "", "", "") }}
{% set title %}{% trans "User" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
] %}
{{ inline.dropdown("userId", "single", title, "", null, "userId", "userName", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Session History ID" %}{% endset %}
{{ inline.number('sessionHistoryId', title) }}
{% set title = "Report Type"|trans %}
{% set options = [
{ id: 'audit', value: "Audit"|trans },
{ id: 'debug', value: "Debug"|trans },
{ id: 'sessions', value: "Sessions"|trans },
] %}
{{ inline.dropdown("type", "single", title, "sessions", options, "id", "value", helpText) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Body -->
<div class="card-body session-history-audit">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="sessionHistoryAuditGrid"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'sessionhistory'}) }}">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Session ID" %}</th>
<th>{% trans "Entity" %}</th>
<th>{% trans "Entity ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="card-body session-history-debug d-none">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="sessionHistoryDebugGrid"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'sessionhistory'}) }}">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Session ID" %}</th>
<th>{% trans "Channel" %}</th>
<th>{% trans "Function" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Page" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="card-body session-history-log d-none">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="sessionHistoryLogGrid"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="{{ url_for("report.data", {name: 'sessionhistory'}) }}">
<thead>
<tr>
<th>{% trans "Start Date" %}</th>
<th>{% trans "End Date" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "UserName" %}</th>
<th>{% trans "User Type" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Session ID" %}</th>
<th>{% trans "Browser" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
{% verbatim %}
<script type="text/x-handlebars-template" id="table-array-viewer">
<a class="arrayViewerToggle" href="#"><span class="fa fa-search"></span></a>
<table class="arrayViewer table table-bordered">
<thead>
<tr>
<th>{{ col1 }}</th>
<th>{{ col2 }}</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{ @key }}</td>
<td>{{ this }}</td>
</tr>
{{/each}}
</tbody>
</table>
</script>
{% endverbatim %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
var arrayViewer = Handlebars.compile($('#table-array-viewer').html());
$('[data-toggle="popover"]').popover();
let $report = $('#sessionHistoryFilter');
let $auditDataTable = $('#sessionHistoryAuditGrid');
let auditTable = createAuditTable($auditDataTable);
let $logDataTable = $('#sessionHistoryDebugGrid');
let logTable = createLogTable($logDataTable);
let $sessionDataTable = $('#sessionHistoryLogGrid');
let sessionTable = createSessionTable($sessionDataTable);
let result; // XHR get data result
let reportType = $report.find('#type').val();
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find('form').serialize(),
success: function success(data) {
result = data;
reportType = $report.find('#type').val();
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
if (reportType === 'audit') {
$('.session-history-audit').removeClass('d-none');
$('.session-history-debug').addClass('d-none');
$('.session-history-log').addClass('d-none');
setTabularData(auditTable, result.table);
} else if (reportType === 'debug') {
$('.session-history-audit').addClass('d-none');
$('.session-history-log').addClass('d-none');
$('.session-history-debug').removeClass('d-none');
setTabularData(logTable, result.table);
} else {
$('.session-history-audit').addClass('d-none');
$('.session-history-debug').addClass('d-none');
$('.session-history-log').removeClass('d-none');
setTabularData(sessionTable, result.table);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
if ($report.find('#type').val() === 'audit') {
$('.arrayViewerToggle').click(function () {
$(this).parent().find('.arrayViewer').toggle();
});
}
}
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($auditDataTable.data().url);
});
$("#refreshGrid").click(function () {
reportType = $report.find('#type').val();
if (reportType=== 'audit') {
auditTable.ajax.reload();
} else if (reportType=== 'debug') {
logTable.ajax.reload();
} else {
sessionTable.ajax.reload();
}
});
function createAuditTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'ipAddress'},
{data: 'sessionHistoryId'},
{data: 'entity', responsivePriority: 2},
{
name: 'entityId',
responsivePriority: 2,
data : function (data) {
if (data.entityId === 0) {
return ''
}
return data.entityId;
}
},
{data: 'message'},
{
"data": function (data, type, row, meta) {
if (type != "display")
return "";
return arrayViewer(
{"col1": "{% trans "Property" %}", "col2": "{% trans "Value" %}", "items": data.objectAfter}
);
},
"sortable": false,
responsivePriority: 1
}
]
})
}
function createLogTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'logDate', "render": dataTableDateFromIso},
{data: 'userName'},
{data: 'userId'},
{data: 'ipAddress'},
{data: 'sessionHistoryId'},
{data: 'channel'},
{data: 'function'},
{data: 'type'},
{data: 'page'},
{data: 'message'},
]
})
}
function createSessionTable($dataTable) {
return $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
order: [[0, 'desc']],
data: {},
columns: [
{data: 'startTime', "render": dataTableDateFromIso},
{data: 'endTime', "render": dataTableDateFromIso},
{data: 'duration'},
{data: 'userName'},
{data: 'userType'},
{data: 'ipAddress'},
{data: 'sessionId'},
{data: 'userAgent'},
]
})
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="sessionHistoryReportPreview" class="table table-striped">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User Name" %}</th>
<th>{% trans "User ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Session ID" %}</th>
<th>{% trans "Message" %}</th>
<th>{% trans "Object" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
var outputData = {{ table|json_encode|raw }};
var table = $("#sessionHistoryReportPreview").DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
paging: false,
ordering: false,
info: false,
order: [[0, 'desc']],
searching: false,
data: outputData,
columns: [
{data: 'logDate',},
{data: 'userName'},
{data: 'userId'},
{data: 'ipAddress'},
{data: 'sessionHistoryId'},
{data: 'message'},
{data: 'objectAfter'},
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,94 @@
{#
/**
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "User" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("user.search") },
{ name: "data-search-term", value: "userName" },
{ name: "data-id-property", value: "userId" },
{ name: "data-text-property", value: "userName" },
] %}
{{ forms.dropdown("userId", "single", title, "", null, "userId", "userName", "", "pagedSelect", "", "d", "", attributes) }}
{% set title = "Report Type"|trans %}
{% set options = [
{ id: 'audit', value: "Audit"|trans },
{ id: 'debug', value: "Debug"|trans },
] %}
{{ forms.dropdown("type", "single", title, "audit", options, "id", "value", helpText) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "sessionhistory",
"description": "Session History",
"class": "\\Xibo\\Report\\SessionHistory",
"type": "Report",
"output_type": "table",
"color":"orange",
"fa_icon": "fa-th",
"sort_order": 5,
"hidden": 0,
"category": "Audit",
"feature": "admin",
"adminOnly": 1
}

View File

@@ -0,0 +1,48 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.label }}</td>
<td>{{ item.duration }}</td>
<td>{{ item.count }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,621 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Summary by Layout, Media or Event" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Summary by Layout, Media or Event" %}</span>
<span class="fa fa-info-circle widget-title-info px-1" data-toggle="popover" data-trigger="hover" data-placement="bottom" data-content="{% trans "This chart shows an aggregate duration and number of plays for the selected Layout, Media or Event. Please select your Range and Type below. Where the Range crosses period boundaries a new period is generated - i.e 1 week grouped by hourly produces 24 x 7 periods." %}"></span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="summaryReport">
<!-- Form Filter -->
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{% set range %}{% trans "Select a range" %}{% endset %}
{% set today %}{% trans "Today" %}{% endset %}
{% set yesterday %}{% trans "Yesterday" %}{% endset %}
{% set thisweek %}{% trans "This Week" %}{% endset %}
{% set thismonth %}{% trans "This Month" %}{% endset %}
{% set thisyear %}{% trans "This Year" %}{% endset %}
{% set lastweek %}{% trans "Last Week" %}{% endset %}
{% set lastmonth %}{% trans "Last Month" %}{% endset %}
{% set lastyear %}{% trans "Last Year" %}{% endset %}
{% set options = [
{ filterName: "", reportFilter: range },
{ filterName: "today", reportFilter: today },
{ filterName: "yesterday", reportFilter: yesterday },
{ filterName: "thisweek", reportFilter: thisweek },
{ filterName: "thismonth", reportFilter: thismonth },
{ filterName: "thisyear", reportFilter: thisyear },
{ filterName: "lastweek", reportFilter: lastweek },
{ filterName: "lastmonth", reportFilter: lastmonth },
{ filterName: "lastyear", reportFilter: lastyear },
] %}
{{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }}
{% set title %}{% trans "From Date" %}{% endset %}
{{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }}
{% set title %}{% trans "To Date" %}{% endset %}
{{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set byday %}{% trans "Day" %}{% endset %}
{% set byweek %}{% trans "Week" %}{% endset %}
{% set bymonth %}{% trans "Month" %}{% endset %}
{% set options = [
{ filterName: "byday", groupByFilter: byday },
{ filterName: "byweek", groupByFilter: byweek },
{ filterName: "bymonth", groupByFilter: bymonth },
] %}
{{ inline.dropdown("groupByFilter", "single", title, "", options, "filterName", "groupByFilter", "", "group-by-filter") }}
{% set title %}{% trans "Type" %}{% endset %}
{% set layout %}{% trans "Layout" %}{% endset %}
{% set media %}{% trans "Media" %}{% endset %}
{% set event %}{% trans "Event" %}{% endset %}
{% set options = [
{ typeid: "layout", type: layout },
{ typeid: "media", type: media },
{ typeid: "event", type: event },
] %}
{{ inline.dropdown("type", "single", title, "", options, "typeid", "type") }}
{% set title %}{% trans "Layout" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("layout.search") },
{ name: "data-search-term", value: "layout" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "layoutId" },
{ name: "data-text-property", value: "layout" }
] %}
{{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }}
{% set title %}{% trans "Media" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Media" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("library.search") },
{ name: "data-search-term", value: "media" },
{ name: "data-id-property", value: "mediaId" },
{ name: "data-text-property", value: "name" }
] %}
{{ inline.dropdown("mediaId", "single", title, "", null, "mediaId", "name", helpText, "pagedSelect media-select", "", "m", "", attributes) }}
{% set title %}{% trans "Tag" %} *{% endset %}
{% set helpText %}{% trans "This field is required when the Type selected is Event" %}{% endset %}
{{ inline.input("eventTag", title, "", helpText, "tag-text") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
aria-controls="chartTab" aria-selected="true">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="false">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- CHART TAB-->
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
<div class="chart-container" style="height:550px;">
<canvas id="canvas" style="clear:both; margin-top:25px;" height="70%"></canvas>
</div>
</div>
<!-- TABULAR TAB-->
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="summaryTbl"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="/report/data/summaryReport"
>
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
let $report = $("#summaryReport");
let $dataTable = $('#summaryTbl'); // Datatable
let chart = null; // Chart
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
let type = $("#type");
let groupByFilter = $("#groupByFilter");
let groupByFilterCls = $(".group-by-filter");
let mediaSelect =$(".media-select");
let layoutSelect =$(".layout-select");
let eventTagCls =$(".tag-text");
let reportFilter = $("#reportFilter"); // Report Filter
// Initialize table with empty data
let table = $dataTable.DataTable({
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
ordering: false,
data: {},
columns: [
{
data: 'label',
'sortable': false,
},
{
data: 'duration',
'sortable': false,
},
{
data: 'count',
'sortable': false,
}
],
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Total over all pages
let totalDuration = api.column(1).data().map(Number).reduce((a, b) => a + b, 0);
let totalNumberPlays = api.column(2).data().map(Number).reduce((a, b) => a + b, 0);
let totalDurationPage = api.column(1, { page: 'current'}).data().map(Number)
.reduce((a, b) => a + b, 0);
let totalNumberPlaysPage = api.column(2, { page: 'current'}).data().map(Number)
.reduce((a, b) => a + b, 0);
// Update footer
$(api.column(1).footer())
.html(`${totalDurationPage} (${totalDuration} total)`);
$(api.column(2).footer())
.html(`${Math.floor(totalNumberPlaysPage)} (${Math.floor(totalNumberPlays)} total)`);
},
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find("form").serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
checkFilterAndApply();
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
}
});
// Calculate the difference of number of days of a selected range
let calculateDaysShowHideWarn = function() {
let fromDt = moment($("#statsFromDt").val());
let toDt = moment($("#statsToDt").val());
let days = toDt.diff(fromDt, 'days');
$warning.hide();
if ( days >= 30) {
$warning.show();
}
return true;
};
$("#statsFromDtLink").change( function() {
calculateDaysShowHideWarn();
});
$("#statsToDtLink").change( function() {
calculateDaysShowHideWarn();
});
// Hide / Show FromDt and ToDt
function checkReportFilter(reportFilter) {
let value = reportFilter.val();
let collectionMonth = [
{id: 'byday', description: "{% trans "Day" %}"},
{id: 'byweek', description: "{% trans "Week" %}"},
];
let collectionYear = [
{id: 'byday', description: "{% trans "Day" %}"},
{id: 'byweek', description: "{% trans "Week" %}"},
{id: 'bymonth', description: "{% trans "Month" %}"}
];
if (value === '' || value === undefined) {
// show the from date and to date
$(".stats-from-dt").show();
$(".stats-to-dt").show();
$("#groupByFilter option").remove();
groupByFilterCls.show();
$.each(collectionYear, function(index, item) {
groupByFilter.append(
$("<option></option>")
.text(item.description)
.val(item.id)
);
});
} else {
// hide the from date and to date
$(".stats-from-dt").hide();
$(".stats-to-dt").hide();
// Group by filter
$("#groupByFilter option").remove();
groupByFilterCls.show();
if (value === 'thismonth' || value === 'lastmonth') {
$.each(collectionMonth, function(index, item) {
groupByFilter.append(
$("<option></option>")
.text(item.description)
.val(item.id)
);
});
} else if (value === 'thisyear' || value === 'lastyear') {
$.each(collectionYear, function(index, item) {
groupByFilter.append(
$("<option></option>")
.text(item.description)
.val(item.id)
);
});
groupByFilter.val('bymonth'); //by default
} else {
groupByFilterCls.hide();
}
}
}
let checkFilterAndApply = function() {
reportFilter.off('change').change( function() {
let value = reportFilter.val();
// Hide / Show FromDt and ToDt
checkReportFilter(reportFilter);
// Hide / Show Warning
$warning.hide();
if ( value === '') {
calculateDaysShowHideWarn();
} else if ( value === 'thismonth' || value === 'lastmonth' || value === 'thisyear' || value === 'lastyear') {
$warning.show();
}
});
type.off('change').change( function() {
let value = type.val();
if (value === 'media') {
// show media and clear/hide the layout
$("#layoutId").val("");
$("#layoutId option").remove();
layoutSelect.hide();
$("#eventTag").val("");
eventTagCls.hide();
mediaSelect.show();
} else if (value === 'layout') {
// show layout and clear/hide the media
$("#mediaId").val("");
$("#mediaId option").remove();
mediaSelect.hide();
$("#eventTag").val("");
eventTagCls.hide();
layoutSelect.show();
} else if (value === 'event') {
// clear/hide the media and layout
$("#mediaId").val("");
$("#mediaId option").remove();
$("#layoutId").val("");
$("#layoutId option").remove();
mediaSelect.hide();
layoutSelect.hide();
// show tag
eventTagCls.show();
}
});
};
// Enable/Disable Schedule Btn
let checkEnableSchedule = function() {
let mediaVal = $("#mediaId").val();
let layoutVal = $("#layoutId").val();
let eventTagVal = $("#eventTag").val();
let reportAddBtn = $("button#reportAddBtn");
let typeVal = $("#type").val();
if ( typeVal === 'layout') {
if (layoutVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&layoutId=" + layoutVal + "&reportName=summaryReport" );
reportAddBtn.removeAttr('title');
}
} else if ( typeVal === 'media') {
if (mediaVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&mediaId=" + mediaVal + "&reportName=summaryReport" );
reportAddBtn.removeAttr('title');
}
} else if ( typeVal === 'event') {
if (eventTagVal == null) {
reportAddBtn.addClass('disabled');
reportAddBtn.removeAttr('href');
} else {
reportAddBtn.removeClass('disabled');
reportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?type=" + typeVal + "&eventTag=" + eventTagVal + "&reportName=summaryReport" );
reportAddBtn.removeAttr('title');
}
}
};
type.val('layout');
mediaSelect.hide();
eventTagCls.hide();
// reportFilter.val('');
// groupByFilter.val('byday');
checkReportFilter(reportFilter);
checkFilterAndApply();
$applyBtn.addClass('disabled');
checkEnableSchedule();
// Bind to form change
$report.on('change', function() {
checkEnableSchedule();
let layoutVal = $("#layoutId").val();
let mediaVal = $("#mediaId").val();
let eventVal = $("#eventTag").val();
if ((layoutVal === null || layoutVal === '' || layoutVal === undefined) &&
(mediaVal === null || mediaVal === '' || mediaVal === undefined) &&
(eventVal === null || eventVal === '' || eventVal === undefined) ) {
$applyBtn.addClass('disabled');
} else {
$applyBtn.removeClass('disabled');
}
});
});
function summaryScheduleCallback(dialog) {
// If we select a displayId we hide the display group filter
$('#reportScheduleAddForm #displayId').off('change').change( function() {
let displayId = $('#reportScheduleAddForm #displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().hide();
} else {
$('#reportScheduleAddForm #displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().show();
}
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2019 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<canvas id="canvas" style="clear:both; margin-top:25px;"></canvas>
</div>
<br/>
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Period" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
let reportChart = new Chart($("#canvas"), {{ chart|json_encode|raw }});
let outputData = {{ table|json_encode|raw }};
// Grid
let table = $("#stats").DataTable({
"searching": false,
"paging": true,
"ordering": false,
"data": outputData,
"columns": [
{ "data": 'label' },
{ "data": 'duration' },
{ "data": 'count' },
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', dataTableProcessing);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{#
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2022 Xibo Signage Ltd
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{{ formTitle }}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}summaryScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{{ forms.dropdown("filter", "single", title, "", filters, "filter", "name", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

14
reports/summary.report Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "summaryReport",
"description": "Chart: Summary by Layout, Media or Event",
"class": "\\Xibo\\Report\\SummaryReport",
"type": "Chart",
"output_type": "both",
"color":"red",
"fa_icon": "fa-bar-chart",
"sort_order": 2,
"hidden": 0,
"category": "Proof of Play",
"feature": "proof-of-play",
"adminOnly": 0
}

View File

@@ -0,0 +1,66 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<span class="small" style="background-color: #d2d2d2; width:150px;">Grey is disconnected %</span>
<span class="small" style="background-color: #5cb85c; width:150px;">Green is connected %</span>
<p></p>
<table class=" ">
<tbody>
{% set displays = tableData.displays %}
{% set timeConnected = tableData.timeConnected %}
{% for key,displayStat in timeConnected %}
<tr>
{% for option in displays[key] %}
<th colspan="2">{{ option }}</th>
{% endfor %}
</tr>
{% for item in displayStat %}
<tr>
{% for displayData in item %}
{% set percent = "0%" %}
{% if displayData.percent > 0 %}
{% set percent = displayData.percent~"%" %}
{% endif %}
<td style="width:180px; text-align: right">{{ displayData.label }} - {{ displayData.percent }}%</td>
<td>
<progress id="file" value="{{ displayData.percent }}" max="100"> {{ displayData.percent }}% </progress>
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,220 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Time Connected" %} | {% endblock %}
{% block actionMenu %}
<style>
table {
table-layout: fixed;
border-collapse: collapse;
border-spacing: 20px 0;
padding: 20px 0 0;
}
th {
text-align: center;
}
td.display-label {
padding-right: 10px;
text-align: right;
}
td.display-percent {
border: 1px solid #408640;
}
.note-box {
height: 15px;
width: 15px;
float:left;
}
</style>
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Time Connected" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="pull-right p-3">
<div>
<div class="note-box" style="background-color: #03a9f4;"></div>
<div style="padding-left: 20px">Blue is disconnected %</div>
</div>
<div>
<div class="note-box" style="background-color: #5cb85c;"></div>
<div style="padding-left: 20px">Green is connected %</div>
</div>
</div>
<div class="XiboFilterCustom card bg-light mb-3">
<div class="FilterDiv card-body" id="timeconnected">
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{{ inline.dateRangeFilter("reportFilter", title, "", "", "", "", "") }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set byhour %}{% trans "Hour" %}{% endset %}
{% set bydayofmonth %}{% trans "Day of month" %}{% endset %}
{% set options = [
{ filterName: "byhour", groupByFilter: byhour },
{ filterName: "bydayofmonth", groupByFilter: bydayofmonth },
] %}
{{ inline.dropdown("groupByFilter", "single", title, "", options, "filterName", "groupByFilter", "", "group-by-filter") }}
{% set title %}{% trans "Display/Display Groups" %}{% endset %}
{% set helpText %}{% trans "Please select one or more displays / groups for this notification to be shown on - Layouts will need the notification widget." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" }
] %}
{% set transGroups %}{% trans "Groups" %}{% endset %}
{% set transDisplays %}{% trans "Display" %}{% endset %}
{% set optionGroups = [
{id: "group", label: transGroups},
{id: "display", label: transDisplays}
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, displayGroupIds, {group: defaults.displayGroups, display: defaults.displays}, "displayGroupId", "displayGroup", helpText, "selectPicker", "", "", "", attributes, optionGroups) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<div class="XiboData card pt-3">
<table id="records_table"></table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(function () {
$('[data-toggle="popover"]').popover();
});
let imageLoader = $("#imageLoader");
function setReport() {
$("#records_table").empty();
$.ajax({
type: "get",
url: "{{ url_for("report.data", {name: reportName}) }}",
cache: false,
dataType: "json",
data: $("#timeconnected").find("form").serialize(),
success: function(response) {
setTimeout(function() {
$("#applyBtn").removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
}, 300);
$('.XiboData').find(".form-error").remove();
if (response.success === false) {
$('.XiboData').append('<div class="alert alert-danger form-error">'+ response.message +'</div>');
return;
}
$.each(response.table.timeConnected, function(index, displayStat) {
var $displayHeader = $('<tr>');
$.each(response.table.displays[index], function(i, displayName) {
$displayHeader.append($('<th colspan="2">').text(displayName));
$displayHeader.appendTo('#records_table').html();
});
// Display Statistics
$.each(displayStat, function(periodId, item) {
var $tr = $('<tr>');
$.each(item, function(displayId, displayData) {
var percent= '';
if (displayData.percent > 0) {
percent = Math.round((displayData.percent + Number.EPSILON) * 100) / 100 + "%";
}
var $label = $('<td class="display-label" style="width:300px">').text(displayData.label);
var $percentage = $('<td class="display-percent" style="width:300px">').css({"background-color": "#03a9f4", "color": "white"}).append(
$('<div>').css({"background-color": "#5cb85c", "width": percent}).text(percent)
);
$tr.append($label);
$tr.append($percentage);
});
$tr.appendTo('#records_table').html();
});
});
}
});
}
$(document).ready(function() {
// Init
var applyBtn = $("#applyBtn");
// Enable/Disable Schedule Btn
var checkEnableSchedule = function() {
// Schedule button enable/disable - start
var anchorReportAddBtn = $("button#reportAddBtn");
anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=timeconnected" );
};
// Report Filter
checkEnableSchedule();
// Bind to form change
$("#timeconnected").on('change', function() {
checkEnableSchedule();
});
// Apply
applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
setReport();
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<style>
table {
table-layout: fixed;
border-collapse: collapse;
border-spacing: 20px 0;
padding: 20px 0 0;
}
th {
text-align: center;
}
td.display-label {
padding-right: 10px;
text-align: right;
}
td.display-percent {
border: 1px solid #408640;
}
.note-box {
height: 15px;
width: 15px;
float:left;
}
</style>
<div class="widget-body">
<div class="pull-right">
<div>
<div class="note-box" style="background-color: #03a9f4;"></div>
<div style="padding-left: 20px">Blue is disconnected %</div>
</div>
<div>
<div class="note-box" style="background-color: #5cb85c;"></div>
<div style="padding-left: 20px">Green is Connected %</div>
</div>
</div>
<div class="XiboGrid" id="{{ random() }}" style="margin-top: 40px;">
<div class="XiboData card pt-3">
<table id="" class="">
<tbody>
{% for key,displayStat in table.timeConnected %}
<tr>
{% for option in table.displays[key] %}
<th colspan="2">{{ option }} </th>
{% endfor %}
</tr>
{% for item in displayStat %}
<tr>
{% for displayData in item %}
{% set percent = "0%" %}
{% if displayData.percent > 0 %}
{% set percent = displayData.percent~"%" %}
{% endif %}
<td class="display-label" style="width:300px">{{ displayData.label }}</td>
<td class="display-percent" style="width:300px; background-color:#03a9f4; color:white">
<div style="background-color:#5cb85c; width:{{ percent }}">
{% if displayData.percent > 0 %}{{ displayData.percent }}%{% endif %}
</div>
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Group by" %}{% endset %}
{% set byhour %}{% trans "Hour" %}{% endset %}
{% set bydayofmonth %}{% trans "Day of month" %}{% endset %}
{% set options = [
{ filterName: "byhour", groupByFilter: byhour },
{ filterName: "bydayofmonth", groupByFilter: bydayofmonth },
] %}
{{ forms.dropdown("groupByFilter", "single", title, "", options, "filterName", "groupByFilter", "", "group-by-filter") }}
{% set title %}{% trans "Display/Display Groups" %}{% endset %}
{% set helpText %}{% trans "Please select one or more displays / groups for this notification to be shown on - Layouts will need the notification widget." %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" }
] %}
{% set transGroups %}{% trans "Groups" %}{% endset %}
{% set transDisplays %}{% trans "Display" %}{% endset %}
{% set optionGroups = [
{id: "group", label: transGroups},
{id: "display", label: transDisplays}
] %}
{{ forms.dropdown("displayGroupIds[]", "dropdownmulti", title, displayGroupIds, {group: displayGroups, display: displays}, "displayGroupId", "displayGroup", helpText, "selectPicker", "", "", "", attributes, optionGroups) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "timeconnected",
"description": "Time Connected",
"class": "\\Xibo\\Report\\TimeConnected",
"type": "Report",
"output_type": "table",
"color":"orange",
"fa_icon": "fa-th",
"sort_order": 4,
"hidden": 0,
"category": "Display",
"feature": "displays.reporting",
"adminOnly": 0
}

View File

@@ -0,0 +1,52 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "base-report.twig" %}
{% block content %}
<div>
<span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span>
</div>
<p></p>
<table class="saved-report-table">
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Time Disconnected" %}</th>
<th>{% trans "Time Connected" %}</th>
<th>{% trans "Units" %}</th>
</tr>
{% for item in tableData %}
<tr>
<td>{{ item.displayId }}</td>
<td>{{ item.display }}</td>
<td>{{ item.timeDisconnected }}</td>
<td>{{ item.timeConnected }}</td>
<td>{{ item.postUnits }}</td>
</tr>
{% endfor %}
</table>
<br/>
<span>{{ placeholder }}</span>
<img src="{{ src|raw }}" >
{% endblock %}

View File

@@ -0,0 +1,339 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block title %}{% trans "Report: Time Connected Summary" %} | {% endblock %}
{% block actionMenu %}
{% include "report-schedule-buttons.twig" %}
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<span>{% trans "Time Connected Summary" %}</span>
</div>
{% include "report-selector.twig" %}
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}" data-refresh-on-form-submit="false">
<div class="XiboFilterCustom">
<div class="FilterDiv card-body" id="timeDisconnectedReport">
<form class="form-inline">
{% set title %}{% trans "Range" %}{% endset %}
{{ inline.dateRangeFilter("reportFilter", title, "", "", "", "", "") }}
{% set title %}{% trans "Group By" %}{% endset %}
{% set options = [
{ id: "display", name: "Display"|trans },
{ id: "displayGroup", name: "Display Group"|trans },
] %}
{{ inline.dropdown("groupBy", "single", title, "", options, "id", "name", "") }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "200px" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% if currentUser.featureEnabled("tag.tagging") %}
{% set title %}{% trans "Tags" %}{% endset %}
{% set exactTagTitle %}{% trans "Exact match?" %}{% endset %}
{% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %}
{% set helpText %}{% trans "A comma separated list of tags to filter by. Enter --no-tag to see items without tags." %}{% endset %}
{{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }}
{% endif %}
{% set title %}{% trans "Only show currently logged in?" %}{% endset %}
{{ inline.checkbox("onlyLoggedIn", title) }}
<div class="w-100">
<a id="applyBtn" class="btn btn-success">
<span>{% trans "Apply" %}</span>
</a>
<span id="imageLoader" class=""></span>
<span id="applyWarning" class="text-warning" style="display:none; padding-left: 10px">{% trans "Warning: This may return a lot of data and may take several minutes to process." %}</span>
</div>
</form>
</div>
</div>
<!-- Card Header -->
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="chart-tab" data-toggle="tab" href="#chartTab" role="tab"
aria-controls="chartTab" aria-selected="true">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tabular-tab" data-toggle="tab" href="#tabularTab" role="tab"
aria-controls="tabularTab" aria-selected="false">Tabular</a>
</li>
</ul>
</div>
<!-- Card Body -->
<div class="card-body">
<div class="tab-content">
<!-- CHART TAB-->
<div class="tab-pane active" id="chartTab" role="tabpanel" aria-labelledby="chart-tab">
<div class="chart-container" style="height:550px;">
<canvas id="canvas" style="clear:both; margin-top:25px;" height="70%"></canvas>
</div>
</div>
<!-- TABULAR TAB-->
<div class="tab-pane show" id="tabularTab" role="tabpanel" aria-labelledby="tabular-tab">
<!-- DATATABLE -->
<div class="table-container" id="table_wrapper">
<table id="timeDisconnectedTbl"
class="table xibo-table table-striped table-full-width"
style="width: 100%"
data-url="/report/data/timedisconnectedsummary"
>
<thead>
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Display Group ID" %}</th>
<th>{% trans "Display Group" %}</th>
<th>{% trans "Time Disconnected" %}</th>
<th>{% trans "Time Connected" %}</th>
<th>{% trans "Average Time Disconnected" %}</th>
<th>{% trans "Average Time Connected" %}</th>
<th>{% trans "Availability" %}</th>
<th>{% trans "Units" %}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
$('[data-toggle="popover"]').popover();
let $report = $("#timeDisconnectedReport");
let $dataTable = $('#timeDisconnectedTbl'); // Datatable
let chart = null;
let result; // XHR get data result
let imageLoader = $("#imageLoader");
let $warning = $("#applyWarning");
let $applyBtn = $("#applyBtn");
// Initialize table with empty data
let table = $dataTable.DataTable({
language: dataTablesLanguage,
dom: dataTablesTemplate,
searching: false,
paging: true,
bInfo: false,
stateSave: true,
bDestroy: true,
processing: true,
ordering: false,
data: {},
columns: [
{"data": "displayId"},
{"data": "display"},
{"data": "displayGroupId"},
{"data": "displayGroup"},
{"data": "timeDisconnected", "sortable": false},
{"data": "timeConnected", "sortable": false},
{"data": "avgTimeDisconnected", "sortable": false},
{"data": "avgTimeConnected", "sortable": false},
{"data": "availabilityPercentage", "sortable": false},
{"data": "postUnits", "sortable": false},
],
drawCallback: function () {
if ($('select[name="groupBy"]').val() === 'displayGroup') {
// Hide 'Display ID and Display' columns
$(this.api().columns([0, 1]).visible(false));
$(this.api().columns([2, 3, 6, 7]).visible(true));
} else {
// Hide 'Display Group ID, Display Group, Avg Time Connected/Disconnected' columns
$(this.api().columns([2, 3, 6, 7]).visible(false));
$(this.api().columns([0, 1]).visible(true));
}
},
});
// Get Data
function getData(url) {
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
data: $report.find("form").serialize(),
success: function success(data) {
result = data;
$applyBtn.removeClass('disabled');
imageLoader.removeClass('fa fa-spinner fa-spin loading-icon');
// Based on tab load data
if ($('.nav-tabs .nav-item a.active').attr("href") === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
},
error: function error(xhr, textStatus, _error) {
$applyBtn.removeClass('disabled');
toastr.error(xhr.responseJSON.message);
}
});
}
function setTabularData(table, data) {
table.clear().draw();
if (Object.keys(data).length > 0) {
table.rows.add( data ).draw()
}
}
function setChartData(data) {
setTimeout(function() {
$applyBtn.removeClass('disabled');
}, 300);
if (chart !== undefined && chart !== null) {
chart.destroy();
}
// Create our chart
chart = new Chart($("#canvas"), data);
}
// Tab shown/click load relevant table/chart
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let activeTab = $(e.target).attr("href")
if (result) {
if (activeTab === '#tabularTab') {
setTabularData(table, result.table);
} else {
setChartData(result.chart);
}
}
});
// Apply
$applyBtn.click(function () {
$(this).addClass('disabled');
imageLoader.addClass('fa fa-spinner fa-spin loading-icon');
getData($dataTable.data().url);
});
// If we select a displayId we hide the display group filter
$('#displayId').off('change').change( function() {
let displayId = $('#displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
$('select[name="groupBy[]"] option').remove();
$('select[name="groupBy"]').parent().hide();
} else {
$('#displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
$('select[name="groupBy"]').parent().show();
}
});
$("#refreshGrid").click(function () {
table.ajax.reload();
});
});
function timeDisconnectedScheduleCallback(dialog) {
// If we select a displayId we hide the display group filter
$('#reportScheduleAddForm #displayId').off('change').change( function() {
let displayId = $('#reportScheduleAddForm #displayId').val();
if (displayId) {
$('select[name="displayGroupId[]"] option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().hide();
$('select[name="groupBy[]"] option').remove();
$('select[name="groupBy"]').parent().hide();
} else {
$('#reportScheduleAddForm #displayId option').remove();
$('select[name="displayGroupId[]"]').next(".select2-container").parent().parent().show();
$('select[name="groupBy"]').parent().show();
}
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,114 @@
{#
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "authed.twig" %}
{% import "inline.twig" as inline %}
{% block actionMenu %}
<div class="widget-action-menu pull-right">
<button class="btn btn-info XiboRedirectButton" href="{{ url_for("savedreport.view") }}"><i class="fa fa-eye" aria-hidden="true"></i> {% trans "Saved Reports" %}</button>
</div>
{% endblock %}
{% block pageContent %}
<div class="widget">
<div class="widget-title">
<i class="fa fa-list"></i>
{{ metadata.title }}
<span class="small">({% trans "Generated on: " %}{{ metadata.generatedOn }})</span>
<div><span class="small">{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}</span></div>
<div class="clearfix"></div>
</div>
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData card pt-3">
<table id="stats" class="table table-striped">
<thead>
<tr>
<th>{% trans "Display ID" %}</th>
<th>{% trans "Display" %}</th>
<th>{% trans "Time Disconnected" %}</th>
<th>{% trans "Time Connected" %}</th>
<th>{% trans "Units" %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="widget">
<div class="widget-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboData" style="margin-top: 25px;">
<canvas id="availabilityChart" style="clear:both; margin-top:25px;" height="330"></canvas>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javaScript %}
<script type="text/javascript" nonce="{{ cspNonce }}">
$(document).ready(function() {
var outputData = {{ table|json_encode|raw }};
var table = $("#stats").DataTable({
"language": dataTablesLanguage,
"dom": dataTablesTemplate,
"paging": false,
"ordering": false,
"info": false,
"order": [[1, "asc"]],
"searching": false,
data: outputData,
"columns": [
{"data": "displayId"},
{"data": "display"},
{"data": "timeDisconnected", "sortable": false},
{"data": "timeConnected", "sortable": false},
{"data": "postUnits", "sortable": false}
]
});
table.on('draw', dataTableDraw);
table.on('processing.dt', function(e, settings, processing) {
dataTableProcessing(e, settings, processing);
});
var availabilityChart = new Chart($("#availabilityChart"), {{ chart|json_encode|raw }});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,108 @@
{#
/**
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
#}
{% extends "form-base.twig" %}
{% import "forms.twig" as forms %}
{% block formTitle %}
{% trans "Add Report Schedule" %}
{% endblock %}
{% block formButtons %}
{% trans "Cancel" %}, XiboDialogClose()
{% trans "Save" %}, $("#reportScheduleAddForm").submit()
{% endblock %}
{% block callBack %}timeDisconnectedScheduleCallback{% endblock %}
{% block formHtml %}
<div class="row">
<div class="col-md-12">
<form id="reportScheduleAddForm" class="XiboForm form-horizontal" method="post" action="{{ url_for("reportschedule.add") }}">
{{ forms.hidden("hiddenFields", hiddenFields) }}
{{ forms.hidden("reportName", reportName) }}
{% set title %}{% trans "Name" %}{% endset %}
{% set helpText %}{% trans "The name for this report schedule" %}{% endset %}
{{ forms.input("name", title, "", helpText, "", "required") }}
{% set title %}{% trans "Frequency" %}{% endset %}
{% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %}
{% set daily %}{% trans "Daily" %}{% endset %}
{% set weekly %}{% trans "Weekly" %}{% endset %}
{% set monthly %}{% trans "Monthly" %}{% endset %}
{% set yearly %}{% trans "Yearly" %}{% endset %}
{% set options = [
{ name: "daily", filter: daily },
{ name: "weekly", filter: weekly },
{ name: "monthly", filter: monthly },
{ name: "yearly", filter: yearly },
] %}
{{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }}
{% set title %}{% trans "Display" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("display.search") },
{ name: "data-search-term", value: "display" },
{ name: "data-search-term-tags", value: "tags" },
{ name: "data-id-property", value: "displayId" },
{ name: "data-text-property", value: "display" }
] %}
{{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Display Group" %}{% endset %}
{% set attributes = [
{ name: "data-width", value: "100%" },
{ name: "data-allow-clear", value: "true" },
{ name: "data-placeholder--id", value: null },
{ name: "data-placeholder--value", value: "" },
{ name: "data-search-url", value: url_for("displayGroup.search") },
{ name: "data-search-term", value: "displayGroup" },
{ name: "data-id-property", value: "displayGroupId" },
{ name: "data-text-property", value: "displayGroup" }
] %}
{{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }}
{% set title %}{% trans "Start Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %}
{{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }}
{% set title %}{% trans "End Time" %}{% endset %}
{% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %}
{{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }}
{% set title %}{% trans "Should an email be sent?" %}{% endset %}
{{ forms.checkbox("sendEmail", title, sendEmail) }}
{% set title %}{% trans "Email addresses" %}{% endset %}
{% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %}
{{ forms.inputWithTags("nonusers", title, nonusers, helpText) }}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{
"name": "timedisconnectedsummary",
"description": "Time Connected Summary",
"class": "\\Xibo\\Report\\TimeDisconnectedSummary",
"type": "Report",
"output_type": "both",
"color":"orange",
"fa_icon": "fa-tasks",
"sort_order": 4,
"hidden": 0,
"category": "Display",
"feature": "displays.reporting",
"adminOnly": 0
}