innhold/custom/tools/generate-mock-petitions.php
Ruben a7829982d0 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
2026-02-25 23:11:35 +01:00

289 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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";