Add petition map to medical cannabis petition page
Add anonymous SVG icon for anonymous signers Add Norway fylker GeoJSON for map boundaries Add CSS styles for petition map Add JavaScript for interactive petition map Add .htaccess to block direct access to data files Add petition-map plugin to process and display map data Add documentation for the petition-map plugin Add mock petition data generator tool
This commit is contained in:
parent
1ee0e0f0a0
commit
a7829982d0
10 changed files with 1134 additions and 1 deletions
289
custom/tools/generate-mock-petitions.php
Normal file
289
custom/tools/generate-mock-petitions.php
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Mock petition data generator — DEV ONLY.
|
||||
*
|
||||
* Generates realistic-looking confirmed petition signatures for testing
|
||||
* the map widget. Writes directly to the real CSV (dev environment only).
|
||||
*
|
||||
* Run from host:
|
||||
* podman exec stopplidelsen.no php /var/www/custom/tools/generate-mock-petitions.php
|
||||
*/
|
||||
|
||||
// Safety check: refuse to run outside the dev container
|
||||
$inContainer = file_exists('/var/www/html') && file_exists('/var/www/custom');
|
||||
if (!$inContainer && !in_array('--force', $argv)) {
|
||||
echo "Warning: this script is intended to run inside the dev container.\n";
|
||||
echo " podman exec stopplidelsen.no php /var/www/custom/tools/generate-mock-petitions.php\n";
|
||||
echo " Pass --force to run anyway.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$csvPath = '/var/www/custom/data/petitions/medisinsk-cannabis-pa-resept.csv';
|
||||
|
||||
// ------- Name lists -------
|
||||
|
||||
$firstNames = [
|
||||
'Kari', 'Ola', 'Anne', 'Per', 'Ingrid', 'Lars', 'Kristin', 'Erik',
|
||||
'Marianne', 'Tor', 'Hanne', 'Bjørn', 'Silje', 'Trond', 'Nina', 'Morten',
|
||||
'Lene', 'Håkon', 'Anita', 'Svein', 'Marit', 'Gunnar', 'Tonje', 'Rune',
|
||||
'Bente', 'Terje', 'Camilla', 'Geir', 'Astrid', 'Jon', 'Trine', 'Anders',
|
||||
'Heidi', 'Vegard', 'Siri', 'Frode', 'Marte', 'Steinar', 'Lise', 'Øyvind',
|
||||
'Karianne', 'Petter', 'Tone', 'Kenneth', 'Gro', 'Vidar', 'Line', 'Eivind',
|
||||
'Maria', 'Jonas', 'Ida', 'Martin', 'Katrine', 'Fredrik', 'Kirsten', 'Helge',
|
||||
'Turid', 'Ole', 'Berit', 'Arild', 'Wendy', 'Ragnar', 'Stine', 'Ivar',
|
||||
'Åse', 'Knut', 'Elise', 'Harald', 'Solveig', 'Dag', 'Kristine', 'Leif',
|
||||
'Elin', 'Nils', 'Mia', 'Eirik', 'Randi', 'Sigurd', 'Thea', 'Magnus',
|
||||
];
|
||||
|
||||
$surnames = [
|
||||
'Hansen', 'Johansen', 'Olsen', 'Larsen', 'Andersen', 'Pedersen', 'Nilsen',
|
||||
'Kristiansen', 'Jensen', 'Karlsen', 'Johnsen', 'Pettersen', 'Eriksen',
|
||||
'Berg', 'Haugen', 'Hagen', 'Johannessen', 'Bakke', 'Strand', 'Lie',
|
||||
'Halvorsen', 'Solberg', 'Lund', 'Dahl', 'Moen', 'Sørensen', 'Bøe',
|
||||
'Berge', 'Aas', 'Holm', 'Martinsen', 'Paulsen', 'Jakobsen', 'Stensrud',
|
||||
'Nygaard', 'Christoffersen', 'Amundsen', 'Svendsen', 'Lunde', 'Sæther',
|
||||
'Eide', 'Henriksen', 'Myhre', 'Fjeld', 'Vik', 'Thorsen', 'Bakken',
|
||||
'Aasen', 'Mathisen', 'Elstad', 'Vold', 'Lindqvist', 'Torp', 'Nesse',
|
||||
'Ruud', 'Brekke', 'Midtbø', 'Grøtting', 'Kleven', 'Hauge', 'Skogen',
|
||||
];
|
||||
|
||||
$regions = [
|
||||
'agder', 'akershus', 'buskerud', 'finnmark', 'innlandet',
|
||||
'more_og_romsdal', 'nordland', 'oslo', 'rogaland', 'telemark',
|
||||
'troms', 'trondelag', 'vestfold', 'vestland', 'ostfold',
|
||||
];
|
||||
|
||||
$displayOptions = ['semi', 'semi', 'semi', 'full', 'anonymous']; // weighted
|
||||
|
||||
// ------- Prompt helpers -------
|
||||
|
||||
function prompt(string $question, string $default = ''): string {
|
||||
echo $question;
|
||||
if ($default !== '') echo " [$default]";
|
||||
echo ': ';
|
||||
$line = trim(fgets(STDIN));
|
||||
return $line === '' ? $default : $line;
|
||||
}
|
||||
|
||||
function promptInt(string $question, int $default, int $min = 1, int $max = PHP_INT_MAX): int {
|
||||
while (true) {
|
||||
$val = (int)prompt($question, (string)$default);
|
||||
if ($val >= $min && $val <= $max) return $val;
|
||||
echo " Please enter a number between $min and $max.\n";
|
||||
}
|
||||
}
|
||||
|
||||
function promptFloat(string $question, float $default, float $min = 0.0, float $max = 1.0): float {
|
||||
while (true) {
|
||||
$val = (float)prompt($question, (string)$default);
|
||||
if ($val >= $min && $val <= $max) return $val;
|
||||
echo " Please enter a number between $min and $max.\n";
|
||||
}
|
||||
}
|
||||
|
||||
// ------- Distribution helpers -------
|
||||
|
||||
/**
|
||||
* Generate a biased region distribution.
|
||||
*
|
||||
* @param int $n Total signatures to distribute
|
||||
* @param float $variance 0.0 = perfectly even, 1.0 = highly concentrated (Oslo-heavy)
|
||||
* @return array [region => count]
|
||||
*/
|
||||
function generateRegionDistribution(int $n, float $variance, array $regions): array {
|
||||
// Base weights (approx. Norwegian population distribution, normalized)
|
||||
$baseWeights = [
|
||||
'oslo' => 12.0,
|
||||
'akershus' => 11.5,
|
||||
'vestland' => 9.0,
|
||||
'rogaland' => 8.0,
|
||||
'trondelag' => 7.5,
|
||||
'innlandet' => 5.5,
|
||||
'buskerud' => 5.0,
|
||||
'vestfold' => 4.5,
|
||||
'ostfold' => 4.5,
|
||||
'more_og_romsdal'=> 4.5,
|
||||
'agder' => 4.0,
|
||||
'telemark' => 3.5,
|
||||
'nordland' => 4.0,
|
||||
'troms' => 3.5,
|
||||
'finnmark' => 1.5,
|
||||
];
|
||||
|
||||
// Blend between uniform (variance=0) and biased (variance=1)
|
||||
$uniform = 1.0 / count($regions);
|
||||
$total = array_sum($baseWeights);
|
||||
$weights = [];
|
||||
|
||||
foreach ($regions as $r) {
|
||||
$biased = ($baseWeights[$r] ?? 1.0) / $total;
|
||||
$weights[$r] = (1 - $variance) * $uniform + $variance * $biased;
|
||||
}
|
||||
|
||||
// Normalise
|
||||
$wsum = array_sum($weights);
|
||||
foreach ($weights as &$w) $w /= $wsum;
|
||||
|
||||
// Allocate counts
|
||||
$counts = [];
|
||||
$remaining = $n;
|
||||
$keys = array_keys($weights);
|
||||
|
||||
foreach ($keys as $i => $r) {
|
||||
if ($i === count($keys) - 1) {
|
||||
$counts[$r] = $remaining;
|
||||
} else {
|
||||
$c = (int)round($n * $weights[$r]);
|
||||
$counts[$r] = $c;
|
||||
$remaining -= $c;
|
||||
}
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
// ------- CSV writer -------
|
||||
|
||||
function appendSignature(string $csvPath, array $data): void {
|
||||
$fp = fopen($csvPath, 'a');
|
||||
if (!$fp) {
|
||||
echo "ERROR: Cannot open $csvPath\n";
|
||||
exit(1);
|
||||
}
|
||||
fputcsv($fp, $data, ',', '"', '');
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
function initCsv(string $csvPath): void {
|
||||
$fp = fopen($csvPath, 'w');
|
||||
if (!$fp) {
|
||||
echo "ERROR: Cannot create $csvPath\n";
|
||||
exit(1);
|
||||
}
|
||||
fputcsv($fp, ['timestamp', 'email', 'firstname', 'surname', 'region', 'display', 'status', 'token', 'token_created', 'ip_hash'], ',', '"', '');
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
// ------- Main -------
|
||||
|
||||
echo "\n";
|
||||
echo "╔══════════════════════════════════════════════╗\n";
|
||||
echo "║ Mock Petition Data Generator — DEV ONLY ║\n";
|
||||
echo "╚══════════════════════════════════════════════╝\n\n";
|
||||
|
||||
$existingCount = 0;
|
||||
if (file_exists($csvPath)) {
|
||||
$fp = fopen($csvPath, 'r');
|
||||
fgetcsv($fp, null, ',', '"', ''); // skip header
|
||||
while (fgetcsv($fp, null, ',', '"', '')) $existingCount++;
|
||||
fclose($fp);
|
||||
echo " Current CSV: $csvPath\n";
|
||||
echo " Existing rows: $existingCount\n\n";
|
||||
} else {
|
||||
echo " CSV does not exist yet, will create it.\n\n";
|
||||
}
|
||||
|
||||
$action = prompt(" Action — [a]ppend or [r]eplace (wipes existing data)", 'a');
|
||||
$replace = strtolower($action[0] ?? 'a') === 'r';
|
||||
|
||||
if ($replace && $existingCount > 0) {
|
||||
$confirm = prompt(" ⚠️ This will delete $existingCount existing rows. Type 'yes' to confirm", '');
|
||||
if (strtolower($confirm) !== 'yes') {
|
||||
echo " Aborted.\n";
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
$count = promptInt(" How many mock petitioners to generate", 1000, 1, 100000);
|
||||
|
||||
echo "\n Regional distribution:\n";
|
||||
echo " 0.0 = perfectly even across all fylker\n";
|
||||
echo " 1.0 = highly concentrated (Oslo/Akershus heavy, matches real population)\n";
|
||||
$variance = promptFloat(" Variance (0.0 – 1.0)", 0.7, 0.0, 1.0);
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Generate distribution
|
||||
$dist = generateRegionDistribution($count, $variance, $regions);
|
||||
|
||||
echo " Distribution preview:\n";
|
||||
arsort($dist);
|
||||
foreach ($dist as $r => $c) {
|
||||
$bar = str_repeat('█', (int)round($c / $count * 40));
|
||||
printf(" %-20s %4d %s\n", $r, $c, $bar);
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
$go = prompt(" Generate $count signatures? [y/n]", 'y');
|
||||
if (strtolower($go[0] ?? 'n') !== 'y') {
|
||||
echo " Aborted.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Prepare CSV
|
||||
if ($replace || !file_exists($csvPath)) {
|
||||
initCsv($csvPath);
|
||||
}
|
||||
|
||||
// Generate signatures
|
||||
$fp = fopen($csvPath, 'a');
|
||||
if (!$fp) {
|
||||
echo "ERROR: Cannot open $csvPath for writing\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$generated = 0;
|
||||
$usedEmails = [];
|
||||
|
||||
foreach ($dist as $region => $regionCount) {
|
||||
for ($i = 0; $i < $regionCount; $i++) {
|
||||
$firstname = $GLOBALS['firstNames'][array_rand($GLOBALS['firstNames'])];
|
||||
$surname = $GLOBALS['surnames'][array_rand($GLOBALS['surnames'])];
|
||||
$display = $GLOBALS['displayOptions'][array_rand($GLOBALS['displayOptions'])];
|
||||
|
||||
// Unique-ish email
|
||||
$slug = strtolower($firstname . '.' . $surname . '.' . $generated);
|
||||
$email = $slug . '@mock.test';
|
||||
|
||||
// Timestamp spread over last 60 days
|
||||
$timestamp = $now - random_int(0, 60 * 86400);
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$ipHash = bin2hex(random_bytes(32));
|
||||
|
||||
fputcsv($fp, [
|
||||
$timestamp,
|
||||
$email,
|
||||
$firstname,
|
||||
$surname,
|
||||
$region,
|
||||
$display,
|
||||
'confirmed', // all mock sigs are confirmed
|
||||
$token,
|
||||
$timestamp,
|
||||
$ipHash,
|
||||
], ',', '"', '');
|
||||
|
||||
$generated++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
|
||||
echo "\n ✓ Generated $generated mock signatures.\n";
|
||||
echo " ✓ Written to: $csvPath\n\n";
|
||||
|
||||
// Invalidate map cache (both private cache and public static file)
|
||||
foreach ([
|
||||
'/var/www/custom/data/petition-map-cache.json',
|
||||
'/var/www/custom/assets/petition-map-data.json',
|
||||
] as $cacheFile) {
|
||||
if (file_exists($cacheFile)) {
|
||||
unlink($cacheFile);
|
||||
echo " ✓ Cleared: $cacheFile\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
Loading…
Add table
Add a link
Reference in a new issue