Skip to content

Commit 2cb3e69

Browse files
authored
Reports: Add Proof of Play MySQL grouping (#2737)
1 parent 3b09d00 commit 2cb3e69

File tree

3 files changed

+170
-24
lines changed

3 files changed

+170
-24
lines changed

lib/Report/ProofOfPlay.php

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Xibo\Factory\LayoutFactory;
3333
use Xibo\Factory\MediaFactory;
3434
use Xibo\Factory\ReportScheduleFactory;
35+
use Xibo\Factory\DisplayGroupFactory;
3536
use Xibo\Helper\ApplicationState;
3637
use Xibo\Helper\DateFormatHelper;
3738
use Xibo\Helper\SanitizerService;
@@ -69,6 +70,11 @@ class ProofOfPlay implements ReportInterface
6970
*/
7071
private $reportScheduleFactory;
7172

73+
/**
74+
* @var DisplayGroupFactory
75+
*/
76+
private $displayGroupFactory;
77+
7278
/**
7379
* @var SanitizerService
7480
*/
@@ -94,6 +100,7 @@ public function setFactories(ContainerInterface $container)
94100
$this->mediaFactory = $container->get('mediaFactory');
95101
$this->layoutFactory = $container->get('layoutFactory');
96102
$this->reportScheduleFactory = $container->get('reportScheduleFactory');
103+
$this->displayGroupFactory = $container->get('displayGroupFactory');
97104
$this->sanitizer = $container->get('sanitizerService');
98105

99106
return $this;
@@ -325,6 +332,12 @@ public function getResults(SanitizerInterface $sanitizedParams)
325332
$operator = $sanitizedParams->getString('logicalOperator', ['default' => 'OR']);
326333
$parentCampaignId = $sanitizedParams->getInt('parentCampaignId');
327334

335+
// Group the data by display, display group, or by tag
336+
$groupBy = $sanitizedParams->getString('groupBy');
337+
338+
// Used with groupBy in case we want to filter by specific display groups only
339+
$displayGroupIds = $sanitizedParams->getIntArray('displayGroupId', ['default' => []]);
340+
328341
// Display filter.
329342
try {
330343
// Get an array of display id this user has access to.
@@ -472,7 +485,8 @@ public function getResults(SanitizerInterface $sanitizedParams)
472485
$tags,
473486
$tagsType,
474487
$exactTags,
475-
$operator
488+
$operator,
489+
$groupBy
476490
);
477491
}
478492

@@ -505,7 +519,10 @@ public function getResults(SanitizerInterface $sanitizedParams)
505519
$entry['minStart'] = Carbon::createFromTimestamp($row['minStart'])->format(DateFormatHelper::getSystemFormat());
506520
$entry['maxEnd'] = Carbon::createFromTimestamp($row['maxEnd'])->format(DateFormatHelper::getSystemFormat());
507521
$entry['mediaId'] = $sanitizedRow->getInt('mediaId');
508-
522+
$entry['displayGroup'] = $sanitizedRow->getString('displayGroup');
523+
$entry['displayGroupId'] = $sanitizedRow->getInt('displayGroupId');
524+
$entry['tagName'] = $sanitizedRow->getString('tagName');
525+
$entry['tagId'] = $sanitizedRow->getInt('tagId');
509526
$rows[] = $entry;
510527
}
511528

@@ -541,6 +558,7 @@ public function getResults(SanitizerInterface $sanitizedParams)
541558
* @param $tags string
542559
* @param $tagsType string
543560
* @param $exactTags mixed
561+
* @param $groupBy string
544562
* @return array[array result, date periodStart, date periodEnd, int count, int totalStats]
545563
*/
546564
private function getProofOfPlayReportMySql(
@@ -555,15 +573,15 @@ private function getProofOfPlayReportMySql(
555573
$tags,
556574
$tagsType,
557575
$exactTags,
558-
$logicalOperator
576+
$logicalOperator,
577+
$groupBy
559578
) {
560579
$fromDt = $fromDt->format('U');
561580
$toDt = $toDt->format('U');
562581

563582
// Media on Layouts Ran
564583
$select = '
565584
SELECT stat.type,
566-
display.Display,
567585
stat.parentCampaignId,
568586
campaign.campaign as parentCampaign,
569587
IFNULL(layout.Layout,
@@ -581,10 +599,23 @@ private function getProofOfPlayReportMySql(
581599
stat.tag,
582600
stat.layoutId,
583601
stat.mediaId,
584-
stat.widgetId,
585-
stat.displayId
602+
stat.widgetId
586603
';
587604

605+
// We get the ID and name - either by display, display group or tag
606+
if ($groupBy === 'display') {
607+
$select .= ', display.Display, stat.displayId ';
608+
} else if ($groupBy === 'displayGroup') {
609+
$select .= ', displaydg.displayGroup, displaydg.displayGroupId ';
610+
} else if ($groupBy === 'tag') {
611+
if ($tagsType === 'dg' || $tagsType === 'media') {
612+
$select .= ', taglink.value, taglink.tagId ';
613+
} else {
614+
// For layouts, we need to manually select taglink.tag
615+
$select .= ', taglink.tag AS value, taglink.tagId ';
616+
}
617+
}
618+
588619
$body = '
589620
FROM stat
590621
LEFT OUTER JOIN display
@@ -615,6 +646,17 @@ private function getProofOfPlayReportMySql(
615646
}
616647
}
617648

649+
if ($groupBy === 'displayGroup') {
650+
// Group the data by display group
651+
$body .= 'INNER JOIN `lkdisplaydg` AS linkdg
652+
ON linkdg.DisplayID = display.displayid
653+
INNER JOIN `displaygroup` AS displaydg
654+
ON displaydg.displaygroupId = linkdg.displaygroupId
655+
AND `displaydg`.isDisplaySpecific = 0 ';
656+
} else if ($groupBy === 'tag') {
657+
$body .= $this->groupByTagType($tagsType);
658+
}
659+
618660
$body .= ' WHERE stat.type <> \'displaydown\'
619661
AND stat.end > :fromDt
620662
AND stat.start < :toDt
@@ -799,23 +841,29 @@ private function getProofOfPlayReportMySql(
799841
$body .= ' AND `media`.mediaId IN (' . trim($mediaSql, ',') . ')';
800842
}
801843

844+
// We first implement default groupings
802845
$body .= '
803846
GROUP BY stat.type,
804847
stat.tag,
805-
display.Display,
806848
stat.parentCampaignId,
807-
stat.displayId,
808849
stat.campaignId,
809850
layout.layout,
810851
IFNULL(stat.mediaId, stat.widgetId),
811852
IFNULL(`media`.name, IFNULL(`widgetoption`.value, `widget`.type)),
812-
stat.tag,
813853
stat.layoutId,
814854
stat.mediaId,
815-
stat.widgetId,
816-
stat.displayId
855+
stat.widgetId
817856
';
818857

858+
// Then add the optional groupings
859+
if ($groupBy === 'display') {
860+
$body .= ', display.Display, stat.displayId';
861+
} else if ($groupBy === 'displayGroup') {
862+
$body .= ', displaydg.displayGroupId, displaydg.displayGroup';
863+
} else if ($groupBy === 'tag') {
864+
$body .= ', value, taglink.tagId';
865+
}
866+
819867
$order = '';
820868
if ($columns != null) {
821869
$order = 'ORDER BY ' . implode(',', $columns);
@@ -829,8 +877,8 @@ private function getProofOfPlayReportMySql(
829877
$entry = [];
830878

831879
$entry['type'] = $row['type'];
832-
$entry['displayId'] = $row['displayId'];
833-
$entry['display'] = $row['Display'];
880+
$entry['displayId'] = $row['displayId'] ?? '';
881+
$entry['display'] = $row['Display'] ?? '';
834882
$entry['layout'] = $row['Layout'];
835883
$entry['parentCampaignId'] = $row['parentCampaignId'];
836884
$entry['parentCampaign'] = $row['parentCampaign'];
@@ -843,7 +891,10 @@ private function getProofOfPlayReportMySql(
843891
$entry['widgetId'] = $row['widgetId'];
844892
$entry['mediaId'] = $row['mediaId'];
845893
$entry['tag'] = $row['tag'];
846-
894+
$entry['displayGroupId'] = $row['displayGroupId'] ?? '';
895+
$entry['displayGroup'] = $row['displayGroup'] ?? '';
896+
$entry['tagId'] = $row['tagId'] ?? '';
897+
$entry['tagName'] = $row['value'] ?? '';
847898
$rows[] = $entry;
848899
}
849900

@@ -1157,4 +1208,24 @@ private function getProofOfPlayReportMongoDb(
11571208
'count' => count($rows)
11581209
];
11591210
}
1211+
1212+
/**
1213+
* Add grouping by tag type
1214+
* @param string $tagType
1215+
* @return string
1216+
*/
1217+
private function groupByTagType(string $tagType) : string
1218+
{
1219+
return match ($tagType) {
1220+
'media' => 'INNER JOIN `lktagmedia` AS taglink ON taglink.mediaId = stat.mediaId',
1221+
'layout' => 'INNER JOIN `lktaglayout` ON `lktaglayout`.layoutId = stat.layoutId
1222+
INNER JOIN `tag` AS taglink ON taglink.tagId = `lktaglayout`.tagId',
1223+
'dg' => 'INNER JOIN `lkdisplaydg` AS linkdg
1224+
ON linkdg.DisplayID = display.displayid
1225+
INNER JOIN `displaygroup` AS displaydg
1226+
ON displaydg.displaygroupId = linkdg.displaygroupId
1227+
AND `displaydg`.isDisplaySpecific = 1 INNER JOIN
1228+
`lktagdisplaygroup` AS taglink ON taglink.displaygroupId = displaydg.displaygroupId',
1229+
};
1230+
}
11601231
}

reports/proofofplay-report-form.twig

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@
8080
{% set title %}{% trans "Time" %}{% endset %}
8181
{{ inline.time("statsToDtTime", title, "00:00", "", "stats-to-dt-time") }}
8282

83+
{% set title %}{% trans "Group By" %}{% endset %}
84+
{% set options = [
85+
{ id: "display", name: "Display" },
86+
{ id: "displayGroup", name: "Display Group"|trans },
87+
{ id: "tag", name: "Tag"|trans }
88+
] %}
89+
{{ inline.dropdown("groupBy", "single", title, "", options, "id", "name", "") }}
90+
8391
{% set title %}{% trans "Display" %}{% endset %}
8492
{% set attributes = [
8593
{ name: "data-width", value: "200px" },
@@ -164,7 +172,7 @@
164172
{{ inline.dropdown("type", "single", title, "", options, "typeid", "type") }}
165173

166174
{% set title %}{% trans "Tags from" %}{% endset %}
167-
{% set dg %}{% trans "Display Group" %}{% endset %}
175+
{% set dg %}{% trans "Display" %}{% endset %}
168176
{% set layout %}{% trans "Layout" %}{% endset %}
169177
{% set media %}{% trans "Media" %}{% endset %}
170178
{% set options = [
@@ -219,6 +227,10 @@
219227
<th>{% trans "Type" %}</th>
220228
<th>{% trans "Display ID" %}</th>
221229
<th>{% trans "Display" %}</th>
230+
<th>{% trans "Display Group ID" %}</th>
231+
<th>{% trans "Display Group" %}</th>
232+
<th>{% trans "Tag ID" %}</th>
233+
<th>{% trans "Tag Name" %}</th>
222234
<th>{% trans "Campaign" %}</th>
223235
<th>{% trans "Layout ID" %}</th>
224236
<th>{% trans "Layout" %}</th>
@@ -251,6 +263,10 @@
251263
<th></th>
252264
<th></th>
253265
<th></th>
266+
<th></th>
267+
<th></th>
268+
<th></th>
269+
<th></th>
254270
</tr>
255271
</tfoot>
256272
</table>
@@ -290,6 +306,21 @@
290306
setTimeout(function() {
291307
$("#applyBtn").removeClass('disabled');
292308
}, 300);
309+
310+
// We hide empty columns and display appropriate columns (ie ID and name)
311+
switch ($('select[name="groupBy"]').val()) {
312+
case 'displayGroup':
313+
$(this.api().columns([1, 2, 5, 6]).visible(false));
314+
$(this.api().columns([3, 4]).visible(true));
315+
break;
316+
case 'tag':
317+
$(this.api().columns([1,2, 3, 4]).visible(false));
318+
$(this.api().columns([5, 6]).visible(true));
319+
break;
320+
default:
321+
$(this.api().columns([3, 4, 5, 6]).visible(false));
322+
$(this.api().columns([1, 2]).visible(true));
323+
}
293324
},
294325
filter: false,
295326
"order": [[1, "asc"]],
@@ -298,6 +329,10 @@
298329
{"data": "type"},
299330
{"data": "displayId"},
300331
{"data": "display"},
332+
{"data": "displayGroupId"},
333+
{"data": "displayGroup"},
334+
{"data": "tagId"},
335+
{"data": "tagName"},
301336
{"data": "parentCampaign"},
302337
{"data": "layoutId"},
303338
{"data": "layout"},
@@ -341,28 +376,27 @@
341376
{"data": "duration"},
342377
{"data": "minStart"},
343378
{"data": "maxEnd"}
344-
345379
],
346380
footerCallback: function (row, data, start, end, display) {
347381
let api = this.api();
348382
349383
// Total over all pages
350-
let totalNumberPlays = api.column(9).data().reduce(function (a, b) {
384+
let totalNumberPlays = api.column(13).data().reduce(function (a, b) {
351385
return a + b;
352386
}, 0);
353-
let totalDuration = api.column(11).data().reduce(function (a, b) {
387+
let totalDuration = api.column(15).data().reduce(function (a, b) {
354388
return a + b;
355389
}, 0);
356-
let totalNumberPlaysPage = api.column(9, { page: 'current'}).data().reduce(function (a, b) {
390+
let totalNumberPlaysPage = api.column(13, { page: 'current'}).data().reduce(function (a, b) {
357391
return a + b;
358392
}, 0);
359-
let totalDurationPage = api.column(11, { page: 'current'}).data().reduce(function (a, b) {
393+
let totalDurationPage = api.column(13, { page: 'current'}).data().reduce(function (a, b) {
360394
return a + b;
361395
}, 0);
362396
363397
// Update footer
364-
$(api.column(9).footer()).html(totalNumberPlaysPage + ' (' + totalNumberPlays + ' total)');
365-
$(api.column(11).footer()).html(Math.floor(totalDurationPage) + ' (' + Math.floor(totalDuration) + ' total)');
398+
$(api.column(13).footer()).html(totalNumberPlaysPage + ' (' + totalNumberPlays + ' total)');
399+
$(api.column(15).footer()).html(Math.floor(totalDurationPage) + ' (' + Math.floor(totalDuration) + ' total)');
366400
},
367401
});
368402
@@ -442,16 +476,38 @@
442476
getData($dataTable.data().url);
443477
});
444478
445-
// If we select a displayId we hide the display group filter
479+
// If we select a displayId, we hide the display group filter
446480
$('#displayId').off('change').change( function() {
447-
448481
let displayId = $('#displayId').val();
482+
449483
if (displayId) {
450484
$('select[name="displayGroupId[]"] option').remove();
451485
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
486+
$('select[name="groupBy[]"] option').remove();
487+
$('select[name="groupBy"]').parent().hide();
452488
} else {
453489
$('#displayId option').remove();
454490
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
491+
$('select[name="groupBy"]').parent().show();
492+
}
493+
});
494+
495+
// If we select a groupBy data, we hide the display filter
496+
$("select[name='groupBy']").on('change', function() {
497+
let optionSelected = $(this).find("option:selected").val();
498+
499+
if (optionSelected === 'displayGroup') {
500+
$('select[name="groupBy"]').parent().show();
501+
$('select[name="displayGroupId[]"]').next(".select2-container").parent().show();
502+
} else {
503+
$('select[name="displayGroupId[]"] option').remove();
504+
$('select[name="displayGroupId[]"]').next(".select2-container").parent().hide();
505+
}
506+
507+
if (optionSelected === 'display') {
508+
$("select[name='displayId']").parent().show();
509+
} else {
510+
$("select[name='displayId']").parent().hide();
455511
}
456512
});
457513

tests/XMDS.http

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ Content-Type: application/xml
166166

167167
###
168168

169+
POST {{url}}/xmds.php?v=7
170+
Content-Type: application/xml
171+
172+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
173+
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
174+
xmlns:tns="urn:xmds" xmlns:types="urn:xmds/encodedTypes"
175+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
176+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
177+
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
178+
<tns:SubmitStats>
179+
<serverKey xsi:type="xsd:string">{{serverKey}}</serverKey>
180+
<hardwareKey xsi:type="xsd:string">{{hardwareKey}}</hardwareKey>
181+
<statXml xsi:type-="xsd:string">&lt;records&gt;&lt;stat fromdt=&quot;2024-08-01 00:00:00&quot; todt=&quot;2024-08-01 00:05:00&quot; type=&quot;layout&quot; scheduleid=&quot;48&quot; layoutid=&quot;133&quot; mediaid=&quot;null&quot; tag=&quot;&quot; count=&quot;250&quot; /&gt;&lt;/records&gt;</statXml>
182+
</tns:SubmitStats>
183+
</soap:Body>
184+
</soap:Envelope>
185+
186+
###
187+
169188
# Get the fileID from the Required Files response.
170189
GET {{url}}/xmds.php?file=12.xlf&displayId=1&type=L&itemId=12&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230502T000000Z&X-Amz-Expires=1683048895&X-Amz-SignedHeaders=host&X-Amz-Signature=7c876be170afb29d194e7b035be6969198c22a32c22e163e2696754bb1163f5d
171190

0 commit comments

Comments
 (0)