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
289 lines
9.3 KiB
PHP
289 lines
9.3 KiB
PHP
#!/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";
|