Skip to content

Commit 3ac62ed

Browse files
authored
Merge pull request #27 from moreonion/target-stats
Implement an exporter for target message counts
2 parents b2467cd + d2dd12c commit 3ac62ed

File tree

5 files changed

+236
-4
lines changed

5 files changed

+236
-4
lines changed

campaignion_csv.module

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ use Drupal\campaignion_csv\DirectoryManager;
99
use Drupal\campaignion_csv\Exporter\ActivityExporter;
1010
use Drupal\campaignion_csv\Exporter\ContactExporter;
1111
use Drupal\campaignion_csv\Exporter\OptInExporter;
12+
use Drupal\campaignion_csv\Exporter\TargetStatsExporter;
1213
use Drupal\campaignion_csv\Exporter\WebformGeneric\Exporter as WebformExporter;
1314
use Drupal\campaignion_csv\Files\ContactRangeFilePattern;
1415
use Drupal\campaignion_csv\Files\MonthlyFilePattern;
16+
use Drupal\campaignion_csv\Files\NodeQueryFilePattern;
17+
use Drupal\campaignion_csv\Files\YearlyFilePattern;
1518

1619
/**
1720
* Implements hook_cronapi().
@@ -112,6 +115,18 @@ function campaignion_csv_campaignion_csv_info() {
112115
'opt_in' => FALSE,
113116
],
114117
];
118+
$export['target_message_count'] = [
119+
'file_pattern' => [
120+
'class' => NodeQueryFilePattern::class,
121+
'date_pattern_class' => YearlyFilePattern::class,
122+
'path' => 'targets/node!nid-%Y.csv',
123+
'retention_period' => new \DateInterval('P24M'),
124+
'refresh_interval' => new \DateInterval('PT23H30M'),
125+
],
126+
'exporter' => [
127+
'class' => TargetStatsExporter::class,
128+
],
129+
];
115130
return $export;
116131
}
117132

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace Drupal\campaignion_csv\Exporter;
4+
5+
use Drupal\campaignion_csv\Files\CsvFileInterface;
6+
use Drupal\campaignion_csv\Timeframe;
7+
8+
/**
9+
* Export per-node message stats for email-to-target targets.
10+
*/
11+
class TargetStatsExporter {
12+
13+
/**
14+
* The timeframe for the current export.
15+
*
16+
* @var \Drupal\campaignion_csv\Timeframe
17+
*/
18+
protected $timeframe;
19+
20+
/**
21+
* Create a new exporter based on the info.
22+
*/
23+
public static function fromInfo(array $info) {
24+
return new static($info['nid'], $info['timeframe']);
25+
}
26+
27+
/**
28+
* Create a new instance.
29+
*
30+
* @param int $nid
31+
* The node to export target stats for.
32+
* @param \Drupal\campaignion_csv\Timeframe $timeframe
33+
* Export data for submissions within this timeframe.
34+
*/
35+
public function __construct(int $nid, Timeframe $timeframe) {
36+
$this->timeframe = $timeframe;
37+
$this->nid = $nid;
38+
}
39+
40+
/**
41+
* Create a sorted list of targets and message counts.
42+
*/
43+
protected function readTargetData() {
44+
list($start, $end) = $this->timeframe->getTimestamps();
45+
$sql = <<<SQL
46+
SELECT s.sid, d.data
47+
FROM webform_submissions s
48+
INNER JOIN webform_component c USING(nid)
49+
INNER JOIN webform_submitted_data d USING(nid, sid, cid)
50+
WHERE s.nid=:nid AND s.is_draft=0 AND c.type='e2t_selector' AND s.submitted BETWEEN :start AND :end AND s.sid>:last_sid
51+
LIMIT 10000
52+
SQL;
53+
$last_sid = 0;
54+
$args = [':start' => $start, ':end' => $end - 1, ':nid' => $this->nid];
55+
$targets = [];
56+
while ($rows = db_query($sql, $args + [':last_sid' => $last_sid])->fetchAll()) {
57+
foreach ($rows as $row) {
58+
$data = unserialize($row->data);
59+
$target = $data['target'];
60+
if (!isset($targets[$target['id']])) {
61+
$targets[$target['id']]['# messages'] = 0;
62+
}
63+
$targets[$target['id']] += array_filter([
64+
'ID' => $target['id'],
65+
'Display name' => $data['message']['display'],
66+
'Salutation' => $target['salutation'],
67+
'Party' => $target['party'] ?? $target['political_affiliation'] ?? '',
68+
'Area / Constituency' => $target['area']['name'] ?? '',
69+
'Area type' => $target['area']['type'] ?? '',
70+
'Area code' => $target['area']['gss_code'] ?? '',
71+
'Country' => $target['area']['country__name'] ?? $target['constituency']['country']['name'] ?? '',
72+
]);
73+
$targets[$target['id']]['# messages'] += 1;
74+
$last_sid = $row->sid;
75+
}
76+
}
77+
usort($targets, function ($a, $b) {
78+
// Exchanging b and a here reverses the sort order: big values first.
79+
return $b['# messages'] <=> $a['# messages'];
80+
});
81+
return $targets;
82+
}
83+
84+
/**
85+
* Write the data to the CsvFile.
86+
*/
87+
public function writeTo(CsvFileInterface $file) {
88+
$header = [
89+
'ID',
90+
'Display name',
91+
'Salutation',
92+
'Party',
93+
'Area / Constituency',
94+
'Area type',
95+
'Area code',
96+
'Country',
97+
'# messages',
98+
];
99+
$file->writeRow($header);
100+
101+
foreach ($this->readTargetData() as $target) {
102+
$row = array_map(function ($key) use ($target) {
103+
return $target[$key] ?? '';
104+
}, $header);
105+
$file->writeRow($row);
106+
}
107+
}
108+
109+
}

src/Files/DateIntervalFilePattern.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ public function __construct($path, \DatePeriod $period, \DateInterval $interval,
6565
$this->info = $info;
6666
}
6767

68+
/**
69+
* Iterate over all configured timeframes.
70+
*/
71+
public function iterateTimeframes() {
72+
foreach ($this->period as $start) {
73+
yield new Timeframe($start, $this->interval);
74+
}
75+
}
76+
6877
/**
6978
* Expand the file pattern and create the specfic files.
7079
*
@@ -77,12 +86,12 @@ public function __construct($path, \DatePeriod $period, \DateInterval $interval,
7786
*/
7887
public function expand($root) {
7988
$files = [];
80-
$interval = $this->interval;
81-
foreach ($this->period as $start) {
82-
$path = strftime($this->pathPattern, $start->getTimestamp());
89+
foreach ($this->iterateTimeframes() as $timeframe) {
90+
list($start, $end) = $timeframe->getTimestamps();
91+
$path = strftime($this->pathPattern, $start);
8392
$info = [
8493
'path' => $root . '/' . $path,
85-
'timeframe' => new Timeframe($start, $interval),
94+
'timeframe' => $timeframe,
8695
] + $this->info;
8796
$files[$path] = TimeframeFileInfo::fromInfo($info);
8897
}

src/Files/NodeQueryFilePattern.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Drupal\campaignion_csv\Files;
4+
5+
/**
6+
* Expand a file pattern for every node based on a timeframe query.
7+
*/
8+
class NodeQueryFilePattern implements FilePatternInterface {
9+
10+
/**
11+
* Create a new instance from an info-array.
12+
*
13+
* @param array $info
14+
* The info-array as defined in hook_campaignion_csv_info().
15+
* @param \DateTimeInterface $now
16+
* The time considered to be now. Defaults to the date and time.
17+
*/
18+
public static function fromInfo(array $info, \DateTimeInterface $now = NULL) {
19+
$node_query = <<<SQL
20+
SELECT s.nid
21+
FROM webform_submissions s
22+
INNER JOIN webform_component c USING(nid)
23+
INNER JOIN webform_submitted_data d USING(nid, sid, cid)
24+
WHERE c.type='e2t_selector' AND s.submitted BETWEEN :start AND :end
25+
GROUP BY s.nid
26+
SQL;
27+
$date_pattern = $info['date_pattern_class']::fromInfo($info);
28+
return new static($info['path'], $node_query, $date_pattern, $info);
29+
}
30+
31+
/**
32+
* Create a new instance.
33+
*/
34+
public function __construct(string $path, string $node_query, DateIntervalFilePattern $date_pattern, array $info) {
35+
$this->pathPattern = $path;
36+
$this->datePattern = $date_pattern;
37+
$this->nodeQuery = $node_query;
38+
$this->info = $info;
39+
}
40+
41+
/**
42+
* Expand the file pattern and create the specfic files.
43+
*
44+
* @param string $root
45+
* The path to the root-directory. The pattern is interpreted relative to
46+
* the root-directory.
47+
*
48+
* @return \Drupal\campaignion_csv\ExportableFileInfoInterface[]
49+
* Array of file info objects keyed by their expanded path.
50+
*/
51+
public function expand($root) {
52+
$files = [];
53+
foreach ($this->datePattern->iterateTimeFrames() as $timeframe) {
54+
list($start, $end) = $timeframe->getTimestamps();
55+
foreach (db_query($this->nodeQuery, [':start' => $start, ':end' => $end])->fetchCol() as $nid) {
56+
$path = strftime(strtr($this->pathPattern, ['!nid' => $nid]), $start);
57+
$info = [
58+
'nid' => $nid,
59+
'path' => $root . '/' . $path,
60+
'timeframe' => $timeframe,
61+
] + $this->info;
62+
$files[$path] = TimeframeFileInfo::fromInfo($info);
63+
}
64+
}
65+
return $files;
66+
}
67+
68+
}

src/Files/YearlyFilePattern.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Drupal\campaignion_csv\Files;
4+
5+
/**
6+
* File pattern that creates yearly files.
7+
*/
8+
class YearlyFilePattern extends DateIntervalFilePattern {
9+
10+
/**
11+
* Create a new instance from an info-array.
12+
*
13+
* @param array $info
14+
* The info-array as defined in hook_campaignion_csv_info(). Keys are:
15+
* - path: The path pattern for the file relative to the root. The path
16+
* is expanded using `strftime()`.
17+
* - retention_period: A \DateInterval specifying how long old files should
18+
* be kept (or generated).
19+
* - include_current: Whether the ongoing year should be included.
20+
* - refresh_interval: A \DateInterval that defines how often files for the
21+
* ongoing period are regenerated.
22+
* @param \DateTimeInterface $now
23+
* The time considered to be now. Defaults to the date and time.
24+
*/
25+
public static function fromInfo(array $info, \DateTimeInterface $now = NULL) {
26+
$info['interval'] = new \DateInterval('P1Y');
27+
$info['anchor_format'] = 'Y-01-01';
28+
return parent::fromInfo($info, $now);
29+
}
30+
31+
}

0 commit comments

Comments
 (0)