290 lines
9.3 KiB
PHP
290 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";
|