Compare commits

...

128 Commits

Author SHA1 Message Date
Kim Taylor
fde39bcb1b Update pledge page template to show elected candidates. 2024-11-25 19:16:49 +11:00
5791273f56 Update template to support elected candidate data 2024-11-18 00:20:50 +11:00
Kim Taylor
45a5afc5f9 Tweak name-match threshold. 2024-11-16 15:19:39 +11:00
Kim Taylor
7af17f0e0b Merge branch 'results' 2024-11-16 14:47:14 +11:00
Kim Taylor
c9ec1933f6 Generate candidates-elected.csv files from results.json. 2024-11-16 14:45:18 +11:00
Kim Taylor
f8fd1cc20c Results parser working for all LGAs (except melbourne) 2024-11-16 12:03:29 +11:00
Kim Taylor
464d617ecc Add parser to scrape winning candidates from VEC site. 2024-11-13 07:24:05 +11:00
Kim Taylor
af457dbd8c Generate all-council page with bold text for LGAs with data. 2024-10-06 17:19:32 +11:00
Kim Taylor
dea1ccfe86 Overrides can delete entries by not specifying a replacement field. 2024-10-05 11:14:26 +10:00
Kim Taylor
abf5147c79 Remove duplicate entries, case-insensitive ward name matches and better override system. 2024-10-03 23:01:50 +10:00
Kim Taylor
6eef04e89f Add ability to override generic survey data. 2024-10-01 23:27:48 +10:00
Kim Taylor
bac9be7003 Allow LGA to put custom text as header. 2024-09-30 22:17:36 +10:00
910bfe85c1 Fix searching for picture with just last name not working 2024-09-28 11:07:21 +10:00
Kim Taylor
b8a29a1de2 Eventually I'll get this right. 2024-09-26 00:37:04 +10:00
Kim Taylor
c47eb353a7 Only update href, not id. 2024-09-26 00:35:46 +10:00
Kim Taylor
51af861f38 Link to LGA pages from pledge page. 2024-09-26 00:18:17 +10:00
Kim Taylor
74c24a37a2 Pick up changes that were on server. 2024-09-25 23:09:08 +10:00
Kim Taylor
802c091ba2 Try using generic survey data for LGA lages. 2024-09-25 23:05:28 +10:00
Kim Taylor
ccae561fda Tweak ward match to put Clive Bury in the right ward. 2024-09-25 22:21:37 +10:00
Kim Taylor
34e0297947 Tweak name matching algo to put Vaughan Williams in the correct LGA. 2024-09-25 22:12:22 +10:00
Kim Taylor
abb4f5675c Fix weird pass-by-reference bug. 2024-09-25 21:37:50 +10:00
Kim Taylor
3708a694ed OR pledge status of generic survey and community survey. 2024-09-25 18:47:39 +10:00
Kim Taylor
9f25e4039e Fix function name collision. 2024-09-25 18:04:25 +10:00
Kim Taylor
84e7d472a9 Use sluggify function to prevent duplicate pledge candidates. 2024-09-25 18:03:13 +10:00
Kim Taylor
73e4ed85d0 Swap priority of pledge images. 2024-09-25 17:49:24 +10:00
Kim Taylor
decc12f381 Landing page shuffles all people who have taken the pledge and uploaded a picture. 2024-09-25 14:58:52 +10:00
Kim Taylor
8543bc1c53 Use default image for candidates who have signed the ledge, but not provided an image. 2024-09-25 14:38:13 +10:00
Kim Taylor
620d1bf06d Always include generic survey pledge results. 2024-09-25 12:18:00 +10:00
Kim Taylor
1f5dc18e81 Force re-fetch of generic response data. 2024-09-25 11:56:30 +10:00
94ea4f6a5f Only search for photos based on a single name if the name is longer than 3 characters 2024-09-25 07:49:16 +10:00
Kim Taylor
72eba5fd58 Homepage image scaling fixed. Apply generic pledge data commit. 2024-09-25 00:16:04 +10:00
Kim Taylor
68938751d4 Fix scoring for generic survey candidates. 2024-09-24 15:26:32 +10:00
Kim Taylor
8b2e66b044 Don't try to match photos if the field is empty. 2024-09-24 15:10:31 +10:00
Kim Taylor
db2d4fcbc7 Don't keep appending .json to image field. 2024-09-24 15:01:44 +10:00
Kim Taylor
d8591d5625 Use generic data as secondary source for pledge page. 2024-09-24 13:23:33 +10:00
Kim Taylor
a30855ac30 Merge branch 'auto_generic' 2024-09-24 00:00:00 +10:00
Kim Taylor
6df6cc2d5e Missing pledge in CSV header. 2024-09-23 23:44:36 +10:00
03652e1285 Preemptively fix ward names for all single ward councils in csv-normaliser 2024-09-23 23:43:22 +10:00
Kim Taylor
c27cc2831b Image fetch and resize working. 2024-09-23 23:25:12 +10:00
fff24136a1 Improve logic for identifying photos in csv-normaliser 2024-09-23 23:06:24 +10:00
45eba00d5d Fix map of queenscliffe not generating 2024-09-23 23:06:24 +10:00
0f05fe20ec Fix Queenscliffe missing from council_names.json 2024-09-23 23:06:24 +10:00
Kim Taylor
f9c151bfae Start generating image download script. 2024-09-22 22:57:35 +10:00
Kim Taylor
219b242503 Generate candidates-generic.csv files based on fuzzy match with LGA/Ward. 2024-09-22 19:46:39 +10:00
Kim Taylor
2f5806a227 Merge branch 'main' into auto_generic 2024-09-22 17:53:38 +10:00
Kim Taylor
e56fc253ba Pick up local change from server worktree. 2024-09-22 17:21:43 +10:00
Kim Taylor
17f2bf0d9a Merge branch 'pledge_page' 2024-09-22 17:17:47 +10:00
Kim Taylor
efecbe1d76 Change pledge text background colour. 2024-09-22 17:08:35 +10:00
Kim Taylor
e2fbd1b1ef Calculate scores based on Faith's criteria. 2024-09-21 19:49:49 +10:00
Kim Taylor
5e8170ecef Highlight pledge text. 2024-09-21 17:44:38 +10:00
Kim Taylor
bbffeb89f1 Update text. 2024-09-21 17:36:04 +10:00
Kim Taylor
cba26faf31 Show photographs. 2024-09-21 17:24:50 +10:00
Kim Taylor
82f65a2050 Generate pledge council index. 2024-09-21 16:53:24 +10:00
7b7317cd55 Handle edge case from EAST GIPPSLAND.csv 2024-09-21 14:45:45 +10:00
1f80ec41db Fix skipping candidates with whitespace around the word "Candidate" in the first column 2024-09-20 22:59:51 +10:00
bb66d8c50e Fix skipping candidates that have whitespace in the first column 2024-09-20 22:04:55 +10:00
Kim Taylor
2453d550ce Fetch generic data from google. 2024-09-20 10:19:57 +10:00
Kim Taylor
af53825c05 Allow case insensitive ward-name match. 2024-09-18 21:47:57 +10:00
Kim Taylor
2e444113c8 Plumb in Matt's PHP template. 2024-09-17 22:19:24 +10:00
Kim Taylor
1412c268b5 Move pledge data parsing to separate file. 2024-09-17 22:02:09 +10:00
Kim Taylor
0f89ddf779 Use pledges.csv files as override of candidates.csv files. 2024-09-17 21:19:26 +10:00
e73afcc366 Remove commas from candidate names before matching to photo 2024-09-17 19:38:42 +10:00
e8e338aeef Don't include candidates who have not been given a score 2024-09-17 19:13:37 +10:00
0867d7916d Update second paragraph on lga pages 2024-09-17 17:59:01 +10:00
80ad01a1b1 Tweak intro paragraph text on lga pages 2024-09-16 22:51:05 +10:00
Kim Taylor
48647998a3 Merge branch 'pledges' 2024-09-14 15:43:24 +10:00
Kim Taylor
b46e331a53 Fix update command. 2024-09-14 15:41:13 +10:00
Kim Taylor
1ab14ba8c9 Separate file for pledge data. 2024-09-14 14:40:52 +10:00
Kim Taylor
b0985b6d60 Apply sed script. 2024-09-10 23:26:00 +10:00
Kim Taylor
18910638a1 Select 9 random candidates for pledge area and generate sed commands. 2024-09-08 22:25:14 +10:00
Kim Taylor
ca0bab5071 Collect data for pledge rotation. 2024-09-08 19:26:09 +10:00
Kim Taylor
90a0fee735 Script to update pledge test page from data in spl-data repo. 2024-09-08 17:49:31 +10:00
61833aacb4 Use CSS class to control candidate tick colour 2024-09-04 23:10:07 +10:00
8043e8c45d Allow each lga to link to their own survey 2024-09-04 21:08:00 +10:00
033b30cb78 Replace ' character when searching for picture file in csv-normaliser 2024-09-04 20:20:15 +10:00
b0b09178e8 Skip lines that have an empty candidate name 2024-09-04 20:19:50 +10:00
6aca0bc039 Check multiple potential input filenames in csv-normaliser 2024-09-04 20:19:12 +10:00
1cadbcffb4 Tweak template to allow custom groups based on groupNames field in config.json 2024-09-03 23:05:03 +10:00
1ea5048790 Ensure rating is always an integer in csv-normaliser 2024-09-03 23:03:44 +10:00
46bad101e0 Improve handling of expected input file names in csv-normaliser 2024-09-03 21:38:16 +10:00
91978de573 Fix csv-normaliser not skipping example lines 2024-09-03 21:21:59 +10:00
40dc385d7b Fix candidate sorting in template 2024-09-03 20:11:29 +10:00
771d8e7aca Hide verbose output in csv-normaliser 2024-09-03 19:57:58 +10:00
36897255a4 Output csv-normaliser "failed to find picture" messages in red 2024-09-03 19:57:58 +10:00
bf5aee2f00 Fix missing newline at end of csv-normaliser output 2024-09-03 19:56:51 +10:00
b4fec83039 Add --folder option to csv-normaliser script to simplify passing in arguments if they are in the same folder 2024-09-03 19:56:40 +10:00
bd6360177f Handle more edge cases in csv-normaliser 2024-09-02 21:42:35 +10:00
c4f8098894 Fix picture size instruction 2024-08-26 22:49:39 +10:00
f1f78c69db Fix typos 2024-08-26 22:26:46 +10:00
e8ce924b8a Update docs 2024-08-26 22:24:29 +10:00
1b5f41319b Add space after map when the list of wards is hidden 2024-08-25 12:05:44 +10:00
239f2781b4 Remove the command to install the wp-cli/restful package from script 2024-08-25 11:53:13 +10:00
5d0cfb3c1b Tweak bulk-upload-media.sh to use spl-data folder instead of each lga folder 2024-08-25 11:52:55 +10:00
c7d9465ba0 Improve label rendering on maps 2024-08-25 11:31:03 +10:00
6d4a49208a Use a CSS class to force columns gap to 0 2024-08-21 23:15:40 +10:00
b21d9d8534 Fix dimensions of ward list so it looks normal on mobile 2024-08-21 23:06:11 +10:00
77dc711f12 Add example-media.json file for testing php-template 2024-08-21 22:59:21 +10:00
eef70107e4 Add list of wards to page if there is more than 1 ward 2024-08-21 22:59:04 +10:00
460b54cdd8 Add map to page if it is present 2024-08-21 22:58:38 +10:00
530554e118 Add description on number of wards 2024-08-21 22:58:12 +10:00
517bc1caf5 Extract sluggify logic to a function 2024-08-21 22:57:38 +10:00
1e45dc212b Fix message when a picture cannot be found not containing a newline 2024-08-21 22:23:18 +10:00
df513aca8e Mount spl-data folder inside csv-normaliser dev container 2024-08-21 22:22:45 +10:00
b4ddd94c93 Improve wrapping of 4 ticks 2024-08-19 14:21:09 +10:00
019090089b Only use ticks under candidates 2024-08-19 14:20:52 +10:00
8cd1463fef Add jq filter for generating LGA page links 2024-08-19 10:52:44 +10:00
86e0e07f0e Tweak paragraph when we don't have any candidates 2024-08-19 10:52:29 +10:00
42ff10d02f Tweak spacing between candidate name and rating 2024-08-19 00:10:14 +10:00
26a9ed4ad4 Show a single red cross for candidates rated 1 or below 2024-08-19 00:09:58 +10:00
6a7291e677 Improve wrapping of candidate rating ticks 2024-08-19 00:09:10 +10:00
2af0779e65 Mark shell scripts as executable 2024-08-18 23:48:59 +10:00
8b3ddec283 Improve LGA template
- Use columns instead of grid so it looks better on mobile
- Add message if there are no candidates for a particular ward
- Tweak spacing of candidate name and ticks
2024-08-18 23:43:58 +10:00
7950bab0c9 Left align opening paragraphs of lga pages 2024-08-18 23:42:11 +10:00
1d012f5ff1 Allow generating page when a candidates.csv file doesn't exist 2024-08-18 23:41:50 +10:00
95b7228778 Created script to capture a map for each council 2024-08-17 23:59:25 +10:00
7436c808e3 Fix handling of MultiPolygon objects 2024-08-17 23:57:25 +10:00
3718378545 Add a script to capture an image of a map 2024-08-17 23:03:47 +10:00
0d2c42da82 Hide the "improve map" link 2024-08-17 22:54:40 +10:00
18257be406 Remove navigation control from map 2024-08-17 22:54:22 +10:00
2028cbbe61 Update map-generator readme 2024-08-17 22:19:35 +10:00
e1a1ff4af3 Show labels in a single layer 2024-08-17 22:15:32 +10:00
f60b1802d5 Auto fit map to boundaries 2024-08-17 22:14:39 +10:00
0a7af5c65b Use a style to control look and feel 2024-08-17 22:12:29 +10:00
56f0f73278 Initial council ward map renderer 2024-08-17 19:25:36 +10:00
2ab9306fff Fix typo in csv normaliser log message 2024-08-15 23:18:28 +10:00
1ce4e9b3b8 Remove filtering based on Council column in csv 2024-08-15 23:16:05 +10:00
b8ea6e8436 Add test files for csv-normaliser script 2024-08-15 22:44:37 +10:00
d9cb183b2a Implement CSV normaliser logic 2024-08-15 22:10:03 +10:00
51 changed files with 3104 additions and 133 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.swp

View File

@@ -2,6 +2,30 @@
SPL tools is a collection of tools to assist in building the Streets People Love website. SPL tools is a collection of tools to assist in building the Streets People Love website.
## csv-normaliser
A PHP script for converting the raw .csv files received from volunteers into a .csv file that can be used by the php-template tool. Ensures that ward names are capitalised correctly, and identifies which picture to use for each candidate.
## map-generator
A webpage built using Mapbox for showing the boundaries of the wards in a particular council. Also contains some JS scripts for generating a .jpg image of each council's map.
## php-template
A PHP script for generating the HTML to use in each council page on the Streets People Love website. Handles reading each of the relevant configuration files and exposing them in a format that can easily be consumed in a regular PHP template. This is used by the make-council-page.sh script.
## VEC Data
This folder contains the raw data downloaded from the VEC Map website.
## bulk-upload-media.sh
A script for uploading all images in the spl-data folder to WordPress. Uses the `upload-media.sh` script to handle uploading each image.
## copy-config.sh
A script for splitting up `council_names.json` into separate `config.json` files for each council. Shouldn't need to be used again.
## council_names.json ## council_names.json
Contains the name of each council, a "short" name, and the names of each ward in the council. Contains the name of each council, a "short" name, and the names of each ward in the council.
@@ -11,7 +35,15 @@ The "short" named is created by taking the electorate name and removing the word
The file can be generated using the `jq` tool and the VEC data: The file can be generated using the `jq` tool and the VEC data:
``` ```
jq '[.[] | {name: .electorateName, electorateId: .electorateId, shortName: .parentElectorateName | match("(.*?)(?:(?: Rural)?(?: City| Shire) Council)").captures[0].string, parentElectorateId: .parentElectorateId, councilName: .parentElectorateName }] | group_by(.parentElectorateId) | map({shortName: .[0].shortName, slug: .[0].shortName | ascii_downcase | split(" ") | join("-"), councilName: .[0].councilName, wardNames: . | map(.name) }) | sort_by(.shortName)' "VEC Data\wards.json" > council_names.json jq '[.[] | {name: .electorateName, electorateId: .electorateId, shortName: .parentElectorateName | match("(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)").captures[0].string, parentElectorateId: .parentElectorateId, councilName: .parentElectorateName }] | group_by(.parentElectorateId) | map({shortName: .[0].shortName, slug: .[0].shortName | ascii_downcase | split(" ") | join("-"), councilName: .[0].councilName, wardNames: . | map(.name) }) | sort_by(.shortName)' "VEC Data\wards.json" > council_names.json
```
## lga-links-filter
This is a jq filter that will output a HTML anchor tag for each council. Can be used like this:
```
jq -f -r .\lga-links-filter .\council_names.json
``` ```
## make-council-pages.sh ## make-council-pages.sh
@@ -21,3 +53,8 @@ This is a bash script for creating a page in WordPress for each council.
If a page for a council already exists, the page will be updated instead. The source of councils for this script is the "council_names.json" file. If a page for a council already exists, the page will be updated instead. The source of councils for this script is the "council_names.json" file.
The script needs the [`jq`](https://jqlang.github.io/jq/), [`php`](https://www.php.net/) and [`wp`](https://wp-cli.org/) tools. The script needs the [`jq`](https://jqlang.github.io/jq/), [`php`](https://www.php.net/) and [`wp`](https://wp-cli.org/) tools.
## upload-media.sh
Tries to upload a file to WordPress and stores the media ID and URL in a .json file next to the file. If the .json file is already present, the upload will be skipped.

16
Updating SPL Website.md Normal file
View File

@@ -0,0 +1,16 @@
# Updating SPL Website
1) Download any candidate pictures provided by volunteer to the appropriate council folder in the `spl-data` repo
1) Ensure that the file name of each picture contains the candidate's full name (eg. for a candidate named `Joe Bloggs`, the file could be named `JoeBloggs.jpg` or `WardName_Joe_Bloggs.png`, etc)
1) Resize the pictures to 400px x 400px
1) Commit the pictures to the `spl-data` repo
1) Download CSV provided by a volunteer containing the candidate scores
1) Normalise the CSV using the "CSV Normaliser" tool (eg. `php csv-normaliser/main.php --input ~/Downloads/COUNCIL_NAME.csv --media ../spl-data/council-name --output ../spl-data/council-name/candidates.csv`)
1) Commit the candidates.csv file to the `spl-data` repo
1) Push commits in the `spl-data` repo to the Git server
1) SSH into the Streets People Love WordPress server
1) Pull the latest changes into the `spl-data` repo
1) Run the `bulk-media-upload.sh` script (eg `sudo ./bulk-media-upload.sh ../spl-data`)
1) Commit any added media .json files in the `spl-data` repo
1) Push commits in the `spl-data` repo to the Git server (use the `spl` git user)
1) Run the `make-council-pages.sh` script to regenerate all pages (eg `sudo ./make-council-pages.sh`) OR regenerate a single page (eg `sudo ./make-council-pages.sh "Council Name"`)

10
bulk-upload-media.sh Normal file → Executable file
View File

@@ -11,20 +11,18 @@ WP_FLAGS="--allow-root --path=/var/www/html"
path="$1" path="$1"
wp package install wp-cli/restful $WP_FLAGS
if test -d "$path"; then if test -d "$path"; then
echo "Found $path, starting upload." echo "Found $path, starting upload."
for file in "$path"/*.jpg; do for file in "$path"/*/*.jpg; do
./upload-media.sh "$file" ./upload-media.sh "$file"
done done
for file in "$path"/*.jpeg; do for file in "$path"/*/*.jpeg; do
./upload-media.sh "$file" ./upload-media.sh "$file"
done done
for file in "$path"/*.png; do for file in "$path"/*/*.png; do
./upload-media.sh "$file" ./upload-media.sh "$file"
done done
for file in "$path"/*.gif; do for file in "$path"/*/*.gif; do
./upload-media.sh "$file" ./upload-media.sh "$file"
done done
else else

0
copy-config.sh Normal file → Executable file
View File

View File

@@ -786,6 +786,14 @@
"Beaufort" "Beaufort"
] ]
}, },
{
"shortName": "Queenscliffe",
"slug": "queenscliffe",
"councilName": "Queenscliffe Borough Council",
"wardNames": [
"Unsubdivided"
]
},
{ {
"shortName": "South Gippsland", "shortName": "South Gippsland",
"slug": "south-gippsland", "slug": "south-gippsland",

144
csv-generic/gen-generic.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
require_once("parse_generic_csv.php");
$options = getopt("", ["generic-csv:", "config-files:"]);
if (isset($options['generic-csv'])) {
$generic_csv = $options['generic-csv'];
} else {
error_log("Error: Missing required option '--generic-csv'.");
exit(1);
}
if (isset($options['config-files'])) {
$config_files = $options['config-files'];
} else {
error_log("Error: Missing required option '--config-files'.");
exit(1);
}
$config_files = explode(" ", $config_files);
$candidate_data = parse_generic_csv($generic_csv);
$lga_list = [];
/* Generate dictionary of LGAs and Wards */
foreach ($config_files as $config_file) {
$config_string = file_get_contents($config_file);
if ($config_string !== FALSE) {
$config = json_decode($config_string, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
$config['config-file'] = $config_file;
$lga_list[] = $config;
}
/* Match user typed LGA/Ward to our database */
match_lga($candidate_data, $lga_list);
/* Calculate score for candidate */
foreach ($candidate_data as $key => $candidate) {
$score = 0;
if ($candidate['Pledge'] === "y") $score++;
if ($candidate['q1'] === "Yes") $score++;
if ($candidate['q3'] === "Yes") $score++;
if ($candidate['q4'] === "Yes") $score++;
if ($candidate['q7'] === "Yes") $score++;
$candidate_data[$key]['Score'] = $score;
}
$header = ["Ward", "Candidate Name", "Rating", "Pledge", "Picture"];
/* Generate candidates-generic.csv */
foreach ($lga_list as $lga) {
$lga_candidates = array_filter($candidate_data, function ($candidate) use ($lga) {
return $candidate['match_lga'] === $lga['slug'];
});
if (count($lga_candidates) === 0) continue;
remove_duplicates($lga_candidates);
$dir = dirname($lga['config-file']);
$dir_files = scandir($dir);
$output_file = $dir."/candidates-generic.csv";
$override_file = $dir."/candidates-override.csv";
if (($handle = fopen($output_file, "w")) === FALSE) {
error_log('Error opening output file');
exit(1);
}
if (fputcsv($handle, $header) === FALSE) {
error_log('Error writing headers to output file');
exit(3);
}
$lines = [];
foreach ($lga_candidates as $candidate) {
/* Add extension to photo hash */
if (strlen($candidate['Photo'])) {
foreach ($dir_files as $file) {
if (preg_match("/\.json$/", $file)) continue;
if (strstr($file, $candidate['Photo'])) {
$candidate['Photo'] = $file;
}
}
}
$lines[] = [
$candidate['match_ward'],
$candidate['Name'],
$candidate['Score'],
$candidate['Pledge'],
$candidate['Photo'],
];
}
/* Apply overrides if they exist */
$overrides = [];
if (file_exists($override_file)) {
if (($ovr_handle = fopen($override_file, "r")) !== FALSE) {
$headers = fgetcsv($ovr_handle);
while (($data = fgetcsv($ovr_handle)) !== FALSE) {
$override = [];
foreach ($headers as $key => $value) {
$override[$value] = $data[$key];
}
$overrides[] = $override;
}
fclose($ovr_handle);
} else {
error_log('Error opening overrides file');
exit(3);
}
}
foreach ($overrides as $override) {
foreach ($lines as $line_key => $line) {
$match_index = array_search($override['Match Field'], $header);
$replace_index = array_search($override['Replace Field'], $header);
if ($line[$match_index] === $override['Match Value']) {
if ($replace_index !== false)
$lines[$line_key][$replace_index] = $override['Replace Value'];
else /* If 'Replace Field' is not matched - delete this entry */
$lines[$line_key]['Delete'] = 'y';
}
}
}
foreach ($lines as $line) {
if (isset($line['Delete'])) continue;
if (fputcsv($handle, $line) === FALSE) {
error_log('Error writing candidate to output file');
exit(3);
}
}
fclose($handle);
}
exit(0);

View File

@@ -0,0 +1,54 @@
<?php
require_once("parse_generic_csv.php");
$options = getopt("", ["generic-csv:", "config-files:"]);
if (isset($options['generic-csv'])) {
$generic_csv = $options['generic-csv'];
} else {
error_log("Error: Missing required option '--generic-csv'.");
exit(1);
}
if (isset($options['config-files'])) {
$config_files = $options['config-files'];
} else {
error_log("Error: Missing required option '--config-files'.");
exit(1);
}
$config_files = explode(" ", $config_files);
$candidate_data = parse_generic_csv($generic_csv);
$lga_list = [];
/* Generate dictionary of LGAs and Wards */
foreach ($config_files as $config_file) {
$config_string = file_get_contents($config_file);
if ($config_string !== FALSE) {
$config = json_decode($config_string, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
$config['config-file'] = $config_file;
$lga_list[] = $config;
}
/* Match user typed LGA/Ward to our database */
match_lga($candidate_data, $lga_list);
$image_map = [];
foreach ($candidate_data as $candidate) {
if (strlen($candidate['photo_url'])) {
$map['url'] = $candidate['photo_url'];
$map['match_lga'] = $candidate['match_lga'];
$image_map[$candidate['Photo']] = $map;
}
}
$json_data = json_encode($image_map);
print_r($json_data);
exit(0);

View File

@@ -0,0 +1,107 @@
<?php
function parse_generic_csv($generic_csv) {
$candidate_data = [];
if (($handle = fopen($generic_csv, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
$question_no = 0;
$is_question = false;
foreach ($headers as $key => $value) {
/* Override key name for questions */
if ($value === "Verified") {
$is_question = false;
}
if (strstr($value, "candidate photo")) $value = "Photo";
if (strstr($value, "In which Local Government Area")) $value = "LGA";
if (strstr($value, "In which Ward")) $value = "Ward";
if (strstr($value, "Political Party")) $value = "Party";
if ($value === "Pledge") {
if (strstr($data[$key], "I pledge")) $data[$key] = "y";
else $data[$key] = "n";
}
if ($value === "Photo") {
$candidate['photo_url'] = $data[$key];
$data[$key] = preg_filter("/.*id=/", "", $data[$key]);
}
if ($is_question) {
$candidate['q'.$question_no++] = $data[$key];
} else {
$candidate[$value] = $data[$key];
}
if ($value === "Pledge") {
$is_question = true;
}
}
$candidate_data[] = $candidate;
}
fclose($handle);
} else {
error_log('Error opening candidates file');
exit(1);
}
return $candidate_data;
}
function match_lga(&$candidate_data, $lga_list) {
foreach ($candidate_data as &$candidate) {
/* Match user typed LGA/Ward to our database */
$max_score = 0;
foreach ($lga_list as $lga) {
$aa = preg_split("/[^a-z]/", strtolower($candidate['LGA']));
$bb = preg_split("/[^a-z]/", $lga['slug']);
$score_sum = 0;
foreach ($aa as $a) {
foreach ($bb as $b) {
similar_text($a, $b, $score);
if ($score > 70) $score_sum += $score;
else $score_sum -= 1;
}
}
if ($score_sum > $max_score) {
$max_score = $score_sum;
$match_lga = $lga;
}
}
$max_score = 0;
foreach ($match_lga['wardNames'] as $ward) {
similar_text(strtolower($ward), strtolower($candidate['Ward']), $score);
if ($score >= $max_score) {
$max_score = $score;
$match_ward = $ward;
}
}
$candidate['match_lga'] = $match_lga['slug'];
$candidate['match_ward'] = $match_ward;
}
}
function remove_duplicates(&$candidate_data) {
$names = [];
$duplicates = [];
foreach ($candidate_data as $candidate_key => $candidate) {
/* If we've already had this name, remove the old entry */
foreach ($names as $name_key => $name) {
similar_text(strtolower($name), strtolower($candidate['Name']), $score);
if ($score > 90) {
$duplicates[] = $name_key;
}
}
$names[$candidate_key] = $candidate['Name'];
}
$duplicates = array_unique($duplicates);
foreach ($duplicates as $duplicate) {
unset($candidate_data[$duplicate]);
}
}

View File

@@ -20,11 +20,15 @@
"label": "Hello Remote World", "label": "Hello Remote World",
"onAutoForward": "notify" "onAutoForward": "notify"
} }
} },
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html" // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html"
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root" // "remoteUser": "root"
"mounts": [
"source=${localWorkspaceFolder}/../../spl-data,target=/workspaces/spl-data,type=bind,consistency=consistent"
]
} }

View File

@@ -6,7 +6,7 @@
"type": "php", "type": "php",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/main.php", "program": "${workspaceFolder}/main.php",
"args": [], "args": ["--input", "${workspaceFolder}/test-input.csv", "--media", "${workspaceFolder}/test-media", "--output", "${workspaceFolder}/test-output.csv",],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"port": 9000 "port": 9000
} }

View File

@@ -1,69 +1,211 @@
<?php <?php
require_once "page_renderer.php"; $options = getopt("", ["folder:", "input:", "output:", "media:"]);
$options = getopt("", ["council-file:", "candidates-file:", "media-file:"]); if (isset($options['folder'])) {
$folder = $options['folder'];
if (isset($options['council-file'])) { if (is_dir($folder)) {
$councilFileContents = file_get_contents($options['council-file']); $expectedInputFileNames = [];
$expectedInputFileNames[] = str_replace("-", " ", strtoupper(basename($folder))) . ".csv";
$expectedInputFileNames[] = strtoupper(basename($folder)) . ".csv";
if (!isset($options['input'])) {
foreach ($expectedInputFileNames as $expectedInputFileName) {
$expectedInputFile = $folder . DIRECTORY_SEPARATOR . $expectedInputFileName;
if (is_file($expectedInputFile)) {
$options['input'] = $expectedInputFile;
}
}
}
if (!isset($options['output'])) {
$options['output'] = $folder . DIRECTORY_SEPARATOR . "candidates.csv";
}
if (!isset($options['media'])) {
$options['media'] = $folder;
}
} else { } else {
error_log("Error: Missing required option '--council-file'."); error_log("Error: Specified folder is not valid.");
exit(1); exit(1);
} }
$councilData = json_decode($councilFileContents, true);
// Check for decoding errors
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Error decoding council file: ' . json_last_error_msg());
exit(1);
} }
if (isset($options['candidates-file'])) { if (isset($options['input'])) {
$candidatesFile = $options['candidates-file']; $inputFile = $options['input'];
} else { } else {
error_log("Error: Missing required option '--candidates-file'."); error_log("Error: Missing required option '--input'.");
exit(1); exit(1);
} }
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary. if (isset($options['output'])) {
$candidateData = []; $outputFile = $options['output'];
if (($handle = fopen($candidatesFile, "r")) !== FALSE) { } else {
$headers = fgetcsv($handle); error_log("Error: Missing required option '--output'.");
exit(1);
}
if (isset($options['media'])) {
$mediaFolder = $options['media'];
} else {
error_log("Error: Missing required option '--media'.");
exit(1);
}
$mediaFiles = scandir($mediaFolder);
if ($mediaFiles === FALSE) {
error_log("Failed to list files in media folder");
exit(1);
}
$candidates = [];
if (($handle = fopen($inputFile, "r")) !== FALSE) {
$currentWard = null;
$currentLine = 0;
while (($data = fgetcsv($handle)) !== FALSE) { while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = []; $currentLine++;
foreach ($headers as $key => $value) { //echo var_dump($data);
$candidate[$value] = $data[$key]; if ($data[0] == "Ward") {
// CSV contains ward names in uppercase, convert them to a more readable form
$currentWard = ucwords(strtolower($data[1]));
// Handle some special cases where the above logic doesn't match the expected names
// Note that we cannot just convert every letter after a '-' character to uppercase
// because there are some ward names like "Bulleke-bek"
if ($currentWard == "Warrk-warrk") {
$currentWard = "Warrk-Warrk";
}
if ($currentWard == "Djirri-djirri") {
$currentWard = "Djirri-Djirri";
}
if ($currentWard == "Coastal-promontory") {
$currentWard = "Coastal-Promontory";
}
if ($currentWard == "Alpine Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Ararat Rural City") $currentWard = "Unsubdivided";
if ($currentWard == "Benalla Rural City") $currentWard = "Unsubdivided";
if ($currentWard == "Campaspe Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Colac Otway Shire") $currentWard = "Unsubdivided";
if ($currentWard == "East Gippsland Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Gannawarra Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Glenelg Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Golden Plains Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Hepburn Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Indigo Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Mansfield Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Melbourne City") $currentWard = "Unsubdivided";
if ($currentWard == "Moira Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Moorabool Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Moyne Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Queenscliffe Borough") $currentWard = "Unsubdivided";
if ($currentWard == "Southern Grampians Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Strathbogie Shire") $currentWard = "Unsubdivided";
if ($currentWard == "Swan Hill Rural City") $currentWard = "Unsubdivided";
if ($currentWard == "Towong Shire") $currentWard = "Unsubdivided";
if ($currentWard == "West Wimmera Shire") $currentWard = "Unsubdivided";
}
if (trim($data[0]) == "Candidate" || trim($data[0]) == "") {
if ($currentWard == null) {
error_log("No ward found, skipping data on line " . $currentLine);
continue;
}
$candidateName = trim($data[1]);
if ($candidateName == "example name" || $candidateName == "") {
error_log("Skipping line ". $currentLine);
continue;
}
//print("Adding candidate " . $candidateName . " to ". $currentWard . "\n");
$name_split = array_values(array_filter(explode(" ", str_replace(",", "", str_replace("'", "_", $data[1]))), function($value) { return !is_null($value) && $value !== ''; }));
$name_patterns = [
".*" . implode(".*", $name_split) . ".*",
".*" . implode(".*", array_reverse($name_split)) . ".*"
];
$first_name = $name_split[0];
$last_name = $name_split[array_key_last($name_split)];
if (strlen($first_name) > 3) {
$name_patterns[] = "^" . $first_name . ".*";
}
if ($last_name != $first_name && strlen($last_name)) {
$name_patterns[] = "^" . $last_name . ".*";
}
$regex_groups = array_map(function($x) { return "(?:" . $x . ")"; }, $name_patterns);
$regex_pattern = "/" . implode("|", $regex_groups) . "/i";
$picture = "";
foreach ($mediaFiles as $mediaFile) {
if ($mediaFile == ".") continue;
if ($mediaFile == "..") continue;
if (preg_match($regex_pattern, $mediaFile)) {
$picture = $mediaFile;
break;
}
}
if ($picture === "") {
print("\033[31mFailed to identify picture for " . $candidateName . "\033[0m\n");
}
$rating = $data[2];
if ($rating == "score" || $rating == "") {
// Don't include candidates who haven't been given a score
continue;
}
$rating = (int)$rating;
array_push(
$candidates,
[
"Ward" => $currentWard,
"Candidate Name" => $candidateName,
"Rating" => $rating,
"Picture" => $picture
]
);
} }
$candidateData[] = $candidate;
} }
fclose($handle); fclose($handle);
} else { } else {
error_log('Error opening candidates file'); error_log('Error opening input file');
exit(1); exit(1);
} }
$candidateData = array_filter($candidateData, function ($candidate) use ($councilData) { if (empty($candidates)) {
return isset($candidate["Council"]) && $candidate["Council"] === $councilData['shortName']; error_log("Failed to find any candidates");
});
if (empty($candidateData)) {
error_log("Failed to load any candidates for " . $councilData['shortName']);
}
if (isset($options['media-file'])) {
$mediaFileContents = file_get_contents($options['media-file']);
} else {
error_log("Error: Missing required option '--media-file'.");
exit(1);
}
$mediaData = json_decode($mediaFileContents, true);
$renderer = new SPLPageRenderer();
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
if ($pageContent === null) {
exit(2); exit(2);
} }
echo $pageContent; if (($handle = fopen($outputFile, "w")) !== FALSE) {
$headers = array(
"Ward",
"Candidate Name",
"Rating",
"Picture"
);
if (fputcsv($handle, $headers) === FALSE) {
error_log('Error writing headers to output file');
exit(3);
}
foreach ($candidates as $candidate) {
$fields = array(
$candidate["Ward"],
$candidate["Candidate Name"],
$candidate["Rating"],
$candidate["Picture"]
);
if (fputcsv($handle, $fields) === FALSE) {
error_log('Error writing candidate to output file');
exit(3);
}
}
} else {
error_log('Error opening output file');
exit(1);
}
print("Data written to " . $outputFile . "\n");
exit(0); exit(0);

View File

@@ -0,0 +1,30 @@
Data for TEST,,,,
Password,,Obtain a password to upload this file from the webmaster of https://streetspeoplelove.org/,,
Your phone number,,"Not for publication, campaign use only",,
Your email,,"Not for publication, campaign use only",,
Ward,BABABI DJINANANG,,,
Candidate,Joe Blogs,4,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Candidate,Jane Doe,Good,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,BOX FOREST,,,
Candidate,First Last,,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,BRUNSWICK WEST,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,BULLEKE-BEK,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,DJIRRI-DJIRRI,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,HARMONY PARK,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,PASCOE VALE SOUTH,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,PENTRIDGE,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,RANDAZZO,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,WARRK-WARRK,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
Ward,WESTBREEN,,,
Candidate, example name,score,Party or Independent,"replace ""example name"" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required. "
,,,,
Get help filling in this form email webmaster@streetspeoplelove.org,,,,
1 Data for TEST
2 Password Obtain a password to upload this file from the webmaster of https://streetspeoplelove.org/
3 Your phone number Not for publication, campaign use only
4 Your email Not for publication, campaign use only
5 Ward BABABI DJINANANG
6 Candidate Joe Blogs 4 Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
7 Candidate Jane Doe Good Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
8 Ward BOX FOREST
9 Candidate First Last Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
10 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
11 Ward BRUNSWICK WEST
12 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
13 Ward BULLEKE-BEK
14 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
15 Ward DJIRRI-DJIRRI
16 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
17 Ward HARMONY PARK
18 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
19 Ward PASCOE VALE SOUTH
20 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
21 Ward PENTRIDGE
22 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
23 Ward RANDAZZO
24 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
25 Ward WARRK-WARRK
26 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
27 Ward WESTBREEN
28 Candidate example name score Party or Independent replace "example name" with a candidate's name. Score should be an integer from 0 to your highest score. Make more of these rows as required.
29
30 Get help filling in this form email webmaster@streetspeoplelove.org

View File

View File

@@ -0,0 +1,4 @@
Ward,"Candidate Name",Rating,Picture
"Bababi Djinanang","Joe Blogs",4,"Joe Blogs.jpg"
"Bababi Djinanang","Jane Doe",Good,"JaneDoe square.png"
"Box Forest","First Last",,LAST_FIRST.jpeg
1 Ward Candidate Name Rating Picture
2 Bababi Djinanang Joe Blogs 4 Joe Blogs.jpg
3 Bababi Djinanang Jane Doe Good JaneDoe square.png
4 Box Forest First Last LAST_FIRST.jpeg

62
get-generic.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
#rclone sync --progress bikewest:spl_generic_survey_2024 $DATA_LOC/google-data
GENERIC_SURVEY=../generic-survey/responses.csv
IMAGES=../generic-survey/images
DATA_PATH="../spl-data"
echo "Fetching latest responses to generic survey."
rm -f $GENERIC_SURVEY # Force re-fetch
rclone -v copyto --drive-export-formats csv 'bikewest:spl_generic_survey_2024/Streets People Love council election candidate pledge and survey (Responses).csv' $GENERIC_SURVEY
config_files=()
for folder in "$DATA_PATH"/*; do
if test -f "$folder"/config.json; then
config_files+=("$folder"/config.json)
fi
done
image_map=$(php csv-generic/gen-image-map.php --generic-csv $GENERIC_SURVEY --config-files "${config_files[*]}")
img_list=()
for key in $(jq -r 'keys[]' <<< $image_map) ; do
if [ -f $IMAGES/$key ] ; then
continue
fi
img_list+=($key)
img_list+=($IMAGES/$key)
done
if [ ${#img_list[*]} -gt 0 ] ; then
echo "Downloading $((${#img_list[*]}/2)) image(s)..."
rclone -v backend copyid bikewest: ${img_list[*]}
fi
for key in $(jq -r 'keys[]' <<< $image_map) ; do
format=$(identify $IMAGES/$key | awk '{print $2}')
case $format in
PNG ) suffix=.png ;;
JPEG ) suffix=.jpg ;;
HEIC ) suffix=.jpg ;;
WEBP ) suffix=.png ;;
*)
echo "Error: Unknown image format: $IMAGES/$key"
;;
esac
lga=$(jq -r ".[\"$key\"][\"match_lga\"]" <<< $image_map)
dst="$DATA_PATH/$lga/$key$suffix"
if [ -f $dst ] ; then
continue
fi
echo "Resizing $dst"
convert $IMAGES/$key -resize 400x400 $dst
done
echo "Generating candidates-generic.csv files."
php csv-generic/gen-generic.php --generic-csv $GENERIC_SURVEY --config-files "${config_files[*]}"

1
lga-links-filter Normal file
View File

@@ -0,0 +1 @@
. | map("<a href=\"" + .slug + "\">" + .councilName + "</a>") | .[]

50
lga-page.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
$options = getopt("", ["candidates-files:", "lga-files:"]);
if (isset($options['candidates-files'])) {
$candidates_files = explode(" ", $options['candidates-files']);
} else {
error_log("Error: Missing required option '--candidates-files'.");
exit(1);
}
if (isset($options['lga-files'])) {
$lga_files = explode(" ", $options['lga-files']);
} else {
error_log("Error: Missing required option '--lga-files'.");
exit(1);
}
$lgas_with_data = [];
foreach ($candidates_files as $file) {
$lgas_with_data[] = basename(dirname($file));
}
print('<!-- wp:list --> <ul class="wp-block-list">' . "\n");
foreach ($lga_files as $config_file) {
$config_string = file_get_contents($config_file);
if ($config_string !== FALSE) {
$config = json_decode($config_string, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
if (array_search($config['slug'], $lgas_with_data) === false)
$font_weight = 300;
else
$font_weight = 700;
print('<!-- wp:list-item {"style":{"typography":{"fontStyle":"normal","fontWeight":"');
print($font_weight . '"}}} -->' . "\n");
print('<li style="font-style:normal;font-weight:' . $font_weight . '">');
print('<a href="' . $config['slug'] . '">' . $config['councilName'] . "</a></li>\n");
print('<!-- /wp:list-item -->' . "\n");
}
print('</ul> <!-- /wp:list -->');
exit(0);

19
make-council-pages.sh Normal file → Executable file
View File

@@ -7,7 +7,7 @@
DATA_PATH="../spl-data" DATA_PATH="../spl-data"
# Controls the flags that are passed to every usage of the wp command. # Controls the flags that are passed to every usage of the wp command.
WP_FLAGS="--allow-root --path=/var/www/html" #WP_FLAGS="--allow-root --path=/var/www/html"
function create_or_update_page() { function create_or_update_page() {
local council_block="$1" local council_block="$1"
@@ -31,7 +31,20 @@ function create_or_update_page() {
jq -n '[inputs | { (input_filename | sub("\\.json$"; "") | sub("^.+/"; "")): . }] | reduce .[] as $item ({}; . + $item)' "${media_inputs[@]}" > "$DATA_PATH"/$slug/media.json jq -n '[inputs | { (input_filename | sub("\\.json$"; "") | sub("^.+/"; "")): . }] | reduce .[] as $item ({}; . + $item)' "${media_inputs[@]}" > "$DATA_PATH"/$slug/media.json
content=$(echo "$council_block" | jq -c | php php-template/main.php --council-file "php://stdin" --candidates-file "$DATA_PATH"/$slug/candidates.csv --media-file "$DATA_PATH"/$slug/media.json ) # Community groups get priority
if test -f "$DATA_PATH"/$slug/candidates.csv; then
candidates_file="$DATA_PATH"/$slug/candidates.csv
else
candidates_file="$DATA_PATH"/$slug/candidates-generic.csv
fi
if test -f "$DATA_PATH"/$slug/candidates-elected.csv; then
candidates_elected_file="$DATA_PATH"/$slug/candidates-elected.csv
echo "Found candidates-elected.csv"
fi
content=$(echo "$council_block" | jq -c | php php-template/main.php --council-file "php://stdin" --candidates-file "$candidates_file" --media-file "$DATA_PATH"/$slug/media.json --candidates-elected-file "$candidates_elected_file" )
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
@@ -40,7 +53,7 @@ function create_or_update_page() {
echo "$content" | wp post update "$page_id" --post_content="$content" $WP_FLAGS - echo "$content" | wp post update "$page_id" --post_content="$content" $WP_FLAGS -
else else
echo "Create page $short_name" echo "Create page $short_name"
echo "$content" | wp post create --post_type=page --post_title="$short_name" --post_status=publish $WP_FLAGS - echo "$content" | wp post create --post_type=page --post_title="$short_name" --post_status=draft $WP_FLAGS -
fi fi
else else

25
make-lga-page.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
# The folder containing data for each council.
# Includes the list of candidates and any media.
DATA_PATH="../spl-data"
# Iterate over folders in data path
candidates_files=()
for folder in "$DATA_PATH"/*; do
if test -f "$folder"/candidates-generic.csv; then
candidates_files+=("$folder"/candidates-generic.csv)
elif test -f "$folder"/candidates.csv; then
candidates_files+=("$folder"/candidates.csv)
fi
if test -f "$folder"/config.json; then
lga_files+=("$folder"/config.json)
fi
done
content=$(php lga-page.php --candidates-files "${candidates_files[*]}" \
--lga-files "${lga_files[*]}")
echo "$content" | wp post update 465 -

25
make-pledge-page.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
# The folder containing data for each council.
# Includes the list of candidates and any media.
DATA_PATH="../spl-data"
DEFAULT_IMAGE="../spl-data/default.png.json"
# Iterate over folders in data path
candidates_files=()
for folder in "$DATA_PATH"/*; do
if test -f "$folder"/candidates-generic.csv; then
candidates_files+=("$folder"/candidates-generic.csv)
fi
# Community groups get priority
if test -f "$folder"/candidates.csv; then
candidates_files+=("$folder"/candidates.csv)
fi
done
content=$(php pledge-update/pledge-page.php --candidates-files "${candidates_files[*]}" \
--default-image $DEFAULT_IMAGE)
echo "$content" | wp post update 12106 -

1
map-generator/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,52 @@
const fs = require('fs');
const { exec } = require('child_process');
var widthArgument = process.argv.at(2);
if (!widthArgument) {
console.log("Defaulting to width of 1080")
width = 1080;
} else {
width = parseInt(widthArgument);
if (isNaN(width)) {
console.log("Invalid width provided");
exit(1);
}
}
var heightArgument = process.argv.at(3);
if (!heightArgument) {
console.log("Defaulting to height of 720")
height = 720;
} else {
height = parseInt(heightArgument);
if (isNaN(height)) {
console.log("Invalid height provided");
exit(1);
}
}
var dataPathArgument = process.argv.at(4);
if (!dataPathArgument) {
console.log("Invalid data path provided");
exit(1);
} else {
dataPath = dataPathArgument;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
(async () => {
var councils = JSON.parse(fs.readFileSync('../council_names.json', 'utf8'));
for (const council of councils) {
console.log("Generating map for " + council.slug + "...");
exec("node ./capture-map.js " + council.slug + " " + width + " " + height + " " + dataPath + "/" + council.slug + "/map.jpg");
// Need to slow down requests to avoid overloading the system...
await sleep(1000);
}
})();

View File

@@ -0,0 +1,61 @@
const path = require('path');
const puppeteer = require('puppeteer-core');
const councilName = process.argv.at(2);
if (!councilName) {
console.log("No council name provided");
exit(1);
}
var widthArgument = process.argv.at(3);
if (!widthArgument) {
console.log("Defaulting to width of 1080")
width = 1080;
} else {
width = parseInt(widthArgument);
if (isNaN(width)) {
console.log("Invalid width provided");
exit(1);
}
}
var heightArgument = process.argv.at(4);
if (!heightArgument) {
console.log("Defaulting to height of 720")
height = 720;
} else {
height = parseInt(heightArgument);
if (isNaN(height)) {
console.log("Invalid height provided");
exit(1);
}
}
var outputPathArgument = process.argv.at(5);
if (!outputPathArgument) {
outputPath = councilName + '.jpg'
console.log("Defaulting to path of '" + path + "'")
} else {
outputPath = outputPathArgument;
}
(async () => {
// Launch the browser and open a new blank page
const browser = await puppeteer.launch({channel: 'chrome'});
const page = await browser.newPage();
// Set screen size.
await page.setViewport({width: width, height: height});
// Navigate the page to a URL. Wait until network activity is finished to ensure map is loaded before taking screenshot.
await page.goto('http://localhost:8000/?council=' + encodeURIComponent(councilName), {
waitUntil: 'networkidle2',
});
// Capture screenshot
await page.screenshot({
path: outputPath
});
await browser.close();
})();

BIN
map-generator/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

4
map-generator/circle.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" id="circle" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">
<path d="M14,7.5c0,3.5899-2.9101,6.5-6.5,6.5S1,11.0899,1,7.5S3.9101,1,7.5,1S14,3.9101,14,7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

106
map-generator/dist/main.js vendored Normal file

File diff suppressed because one or more lines are too long

21
map-generator/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Council Wards 2024</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.mapbox-improve-map { display: none; }
</style>
</head>
<body>
<div id="map"></div>
<script src="dist/main.js"></script>
</body>
</html>

948
map-generator/package-lock.json generated Normal file
View File

@@ -0,0 +1,948 @@
{
"name": "map-generator",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"polylabel": "^2.0.1",
"puppeteer-core": "^23.1.0"
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz",
"integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==",
"dependencies": {
"debug": "^4.3.6",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.4.0",
"semver": "^7.6.3",
"tar-fs": "^3.0.6",
"unbzip2-stream": "^1.4.3",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="
},
"node_modules/@types/node": {
"version": "22.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz",
"integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==",
"optional": true,
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/b4a": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz",
"integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg=="
},
"node_modules/bare-events": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz",
"integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==",
"optional": true
},
"node_modules/bare-fs": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz",
"integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==",
"optional": true,
"dependencies": {
"bare-events": "^2.0.0",
"bare-path": "^2.0.0",
"bare-stream": "^2.0.0"
}
},
"node_modules/bare-os": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz",
"integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==",
"optional": true
},
"node_modules/bare-path": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
"integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
"optional": true,
"dependencies": {
"bare-os": "^2.1.0"
}
},
"node_modules/bare-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz",
"integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==",
"optional": true,
"dependencies": {
"streamx": "^2.18.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/basic-ftp": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": {
"node": "*"
}
},
"node_modules/child_process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
"integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g=="
},
"node_modules/chromium-bidi": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz",
"integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==",
"dependencies": {
"mitt": "3.0.1",
"urlpattern-polyfill": "10.0.0",
"zod": "3.23.8"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1312386",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"engines": {
"node": ">=6"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w=="
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-uri": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",
"integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4",
"fs-extra": "^11.2.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
"integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.0.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.5",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/polylabel": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz",
"integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==",
"dependencies": {
"tinyqueue": "^3.0.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/proxy-agent": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
"integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.3",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.0.1",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/puppeteer-core": {
"version": "23.1.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.1.0.tgz",
"integrity": "sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g==",
"dependencies": {
"@puppeteer/browsers": "2.3.1",
"chromium-bidi": "0.6.4",
"debug": "^4.3.6",
"devtools-protocol": "0.0.1312386",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/queue-tick": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
"dependencies": {
"agent-base": "^7.1.1",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/streamx": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",
"integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==",
"dependencies": {
"fast-fifo": "^1.3.2",
"queue-tick": "^1.0.1",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
"bare-events": "^2.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
"integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^2.1.1",
"bare-path": "^2.1.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/text-decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz",
"integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/typed-query-selector": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="
},
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dependencies": {
"buffer": "^5.2.1",
"through": "^2.3.8"
}
},
"node_modules/undici-types": {
"version": "6.19.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
"integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
"optional": true
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/urlpattern-polyfill": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"polylabel": "^2.0.1",
"puppeteer-core": "^23.1.0"
}
}

17
map-generator/readme.md Normal file
View File

@@ -0,0 +1,17 @@
# Map Generator
This project displays a map showing the boundaries of wards within a single local government area. Each ward is labelled.
The data for this comes from the VEC Map API: https://maps.vec.vic.gov.au/api/electorates/4/withboundaries
To allow the page to download the `wards_withboundaries.json` file, the page needs to be accessed via a `http://` URL (and not a `file:///` URL). One easy way to do this is with the web server built into Python, it can be started by running `python3 -m http.server` in this folder.
To tell the page which council to load, add a query parameter to the URL eg. `?council=brimbank` or `?council=Melbourne%20City%20Council`
To automatically compile the `src.js` file after every edit, run `webpack-cli --mode development --watch` in this folder.
Changes to `dist/main.js` should be committed so that other users don't need to install node.
The `capture-map.js` script can be used to capture an image of the map. It accepts 4 arguments, the council name, width, height, and output path. For example `node .\capture-map.js brimbank 900 500 ../../spl-data/brimbank/map.jpg` would capture a map of Brimbank City Council and place a file named `map.jpg` in the `../../spl-data/brimbank/` folder.
The `capture-all-maps.js` script can be used to capture an image for each council. It accepts 3 arguments, width, height, and the path to the `spl-data` repo. For example `node .\capture-all-maps.js 900 500 ../../spl-data` would capture a map of each council and put it in the spl-data repo in the appropriate folder.

196
map-generator/src.js Normal file
View File

@@ -0,0 +1,196 @@
import polylabel from 'polylabel';
function normaliseCouncilName(str) {
const regex = /(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)/g;
const matches = str.matchAll(regex);
// If we get a match, convert to slug format
for (const match of matches) {
return match[1].toLowerCase().replace(" ", "-");
}
// If we didn't find any matches, try convert input to slug format
return str.toLowerCase().replace(" ", "-");
};
const searchParams = new URLSearchParams(window.location.search);
const councilName = normaliseCouncilName(searchParams.get("council"));
console.log(councilName);
mapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1ODZnNjF4cyJ9.IX8CfYQZUaQhSjWgMXmsEA';
const map = new mapboxgl.Map({
container: 'map',
zoom: 10,
style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',
center: [145.00724,-37.79011]
});
map.on('load', () => {
// Load an image from an external URL.
map.loadImage('circle.png', (error, image) => {
if (error) throw error;
map.addImage('blue-circle', image);
fetch("wards_withboundaries.json")
.then(response => {
response.json()
.then((wardData) => {
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
var bounds = {
"west": undefined,
"south": undefined,
"east": undefined,
"north": undefined
}
function addToBounds(coordinate) {
if (bounds.west == undefined || coordinate[0] < bounds.west) {
bounds.west = coordinate[0];
}
if (bounds.south == undefined || coordinate[1] < bounds.south) {
bounds.south = coordinate[1];
}
if (bounds.east == undefined || coordinate[0] > bounds.east) {
bounds.east = coordinate[0];
}
if (bounds.north == undefined || coordinate[1] > bounds.north) {
bounds.north = coordinate[1];
}
}
var labelFeatures = [];
filteredWardData.forEach(wardData => {
const featureCollection = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': JSON.parse(wardData.boundaryJson)
}
]
};
if (featureCollection.features[0].geometry.type == "Polygon") {
featureCollection.features[0].geometry.coordinates[0].forEach(coordinate => {
addToBounds(coordinate);
});
}
if (featureCollection.features[0].geometry.type == "MultiPolygon") {
featureCollection.features[0].geometry.coordinates.forEach(polygon => {
polygon[0].forEach(coordinate => {
addToBounds(coordinate);
});
});
}
// Add data
map.addSource("data_"+wardData.electorateId, {
'type': 'geojson',
'data': featureCollection
});
// Add a line along the data
map.addLayer({
'id': "outline_"+wardData.electorateId,
'type': 'line',
'source': "data_"+wardData.electorateId,
'layout': {},
'paint': {
'line-color': '#0899fe',
'line-width': 3
}
});
var centrePoint;
if (featureCollection.features[0].geometry.type == "Polygon") {
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001);
}
if (featureCollection.features[0].geometry.type == "MultiPolygon") {
// TODO: Find the biggest polygon in the multipolygon and use that to find the centre point
// instead of just picking the second polygon.
//
// The 2024 set of boundaries only uses 2 MultiPolygon objects (Cathedral in Murrindindi Shire Council and Island in Bass Coast Shire Council)
// Luckily, the second polygon for both objects results in a good label placement.
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates[1], 0.000001);
}
if (wardData.electorateName.includes(' ')) {
// Breaking long names into newlines looks better
const parts = wardData.electorateName.split(' ');
// Special case if a ward starts with "St" (like "St Albans East")
// Join the first two parts
if (parts[0] == "St") {
parts[0] = parts[0] + ' ' + parts[1];
parts.splice(1, 1);
}
const wardNameNewLines = parts.join('\n');
labelFeatures.push({
'type': 'Feature',
'properties': {
'description': wardNameNewLines
},
'geometry': {
'type': 'Point',
'coordinates': centrePoint
}
});
} else {
labelFeatures.push({
'type': 'Feature',
'properties': {
'description': wardData.electorateName
},
'geometry': {
'type': 'Point',
'coordinates': centrePoint
}
});
}
});
map.addSource('labels', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': labelFeatures
}
});
map.addLayer({
'id': 'labels',
'type': 'symbol',
'source': 'labels',
'layout': {
'text-field': ['get', 'description'],
'text-variable-anchor': ['top', 'left', 'bottom', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
'text-radial-offset': 1,
'text-padding': 0,
'text-justify': 'auto',
'text-allow-overlap': false,
'text-ignore-placement': false,
'icon-image': 'blue-circle'
}
});
map.fitBounds([
[bounds.west, bounds.south],
[bounds.east, bounds.north]
], {
padding: 25,
animate: false
});
}).catch(err => {
console.log(err);
});
});
});
});

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"type": "php", "type": "php",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/main.php", "program": "${workspaceFolder}/main.php",
"args": ["--council-file", "${workspaceFolder}/example-config.json", "--candidates-file", "${workspaceFolder}/example-candidates.csv"], "args": ["--council-file", "${workspaceFolder}/example-config.json", "--candidates-file", "${workspaceFolder}/example-candidates.csv", "--media-file", "${workspaceFolder}/example-media.json"],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"port": 9000 "port": 9000
} }

View File

@@ -0,0 +1,2 @@
Ward,Candidate Name,Elected
Harvester,Joe Blogs,y
1 Ward Candidate Name Elected
2 Harvester Joe Blogs y

View File

@@ -0,0 +1,10 @@
{
"map.jpg": {
"id": 123,
"url": "http://localhost/map.png"
},
"default.png": {
"id": 987,
"url": "http://localhost/default.png"
}
}

View File

@@ -1,7 +1,7 @@
<?php <?php
require_once "page_renderer.php"; require_once "page_renderer.php";
$options = getopt("", ["council-file:", "candidates-file:", "media-file:"]); $options = getopt("", ["council-file:", "candidates-file:", "media-file:", "candidates-elected-file:"]);
if (isset($options['council-file'])) { if (isset($options['council-file'])) {
$councilFileContents = file_get_contents($options['council-file']); $councilFileContents = file_get_contents($options['council-file']);
@@ -27,6 +27,7 @@ if (isset($options['candidates-file'])) {
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary. // Convert CSV into an array of dictionaries. Use the header as the key in the dictionary.
$candidateData = []; $candidateData = [];
if (file_exists($candidatesFile)) {
if (($handle = fopen($candidatesFile, "r")) !== FALSE) { if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
$headers = fgetcsv($handle); $headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) { while (($data = fgetcsv($handle)) !== FALSE) {
@@ -41,13 +42,8 @@ if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
error_log('Error opening candidates file'); error_log('Error opening candidates file');
exit(1); exit(1);
} }
} else {
$candidateData = array_filter($candidateData, function ($candidate) use ($councilData) { error_log("The specified candidates.csv file does not exist, will not show any candidates for " . $councilData["shortName"] . ".");
return isset($candidate["Council"]) && $candidate["Council"] === $councilData['shortName'];
});
if (empty($candidateData)) {
error_log("Failed to load any candidates for " . $councilData['shortName']);
} }
if (isset($options['media-file'])) { if (isset($options['media-file'])) {
@@ -59,6 +55,37 @@ if (isset($options['media-file'])) {
$mediaData = json_decode($mediaFileContents, true); $mediaData = json_decode($mediaFileContents, true);
// Merge elected data (if present) into candidate objects
if (isset($options['candidates-elected-file'])) {
$candidatesElectedFile = $options['candidates-elected-file'];
if (file_exists($candidatesElectedFile)) {
if (($handle = fopen($candidatesElectedFile, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$electedCandidate = [];
foreach ($headers as $key => $value) {
$electedCandidate[$value] = $data[$key];
}
foreach ($candidateData as &$candidate) {
if ($candidate["Candidate Name"] == $electedCandidate["Candidate Name"]) {
if ($electedCandidate["Elected"] == "y") {
$candidate["Elected"] = True;
}
break;
}
}
}
fclose($handle);
} else {
error_log('Error opening candidates elected file');
exit(1);
}
} else {
error_log("The specified candidates elected file does not exist, will not show any elected candidates for " . $councilData["shortName"] . ".");
}
}
$renderer = new SPLPageRenderer(); $renderer = new SPLPageRenderer();
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData); $pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
if ($pageContent === null) { if ($pageContent === null) {

View File

@@ -1,9 +1,30 @@
<!-- wp:paragraph {"align":"center"} --> <?php
<p class="has-text-align-center">The Streets People Love campaign has created scorecards for candidates in the 2024 council elections. Scorecards have been generated based on a candidate's engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the local government area in which they are running.</p>
function sluggify($input) {
return strtolower(str_replace(' ', '-', $input));
}
$surveyLink = "<a href=\"https://forms.gle/gnDNyBiVC64tDo2Y7\">Streets People Love Pledge and Survey</a>";
if (isset($config["survey"])) {
$surveyLink = $config["survey"];
}
?>
<!-- wp:paragraph -->
<?php if (isset($config['header'])): ?>
<p><?php echo $config['header']; ?></p>
<?php else: ?>
<p>The Streets People Love campaign has created scorecards for candidates in the 2024 council elections. Scorecards have been generated based on a candidate's engagement with the Streets People Love campaign, their commitment to our pledge, their responses to a survey and input from campaign members located in the local government area in which they are running.</p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} --> <!-- wp:paragraph -->
<p class="has-text-align-center">Have candidates in your local government area not yet taken part? Send your local candidates the <a href="https://forms.gle/gnDNyBiVC64tDo2Y7">Streets People Love Pledge and Survey</a> and ask them to complete it so that local residents can vote for the candidates who want to build the streets people love.</p> <p>Can't see a candidate you know is running? Candidates who don't take our survey won't appear on this page. Feel free to send your local candidates the <?php echo $surveyLink; ?> and let them know it's important to local residents that they do take part, so that we can vote for those who want to build the streets people love.</p>
<?php endif; ?>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
<?php if (isset($media["header.jpg"])): ?> <?php if (isset($media["header.jpg"])): ?>
@@ -12,56 +33,181 @@
<!-- /wp:image --> <!-- /wp:image -->
<?php endif ?> <?php endif ?>
<?php foreach ($config['wardNames'] as $index => $wardName): ?> <?php
$wardCount = count($config['wardNames']);
if ($wardCount > 1) {
$wardsDescription = $config['councilName'] . " is divided into " . $wardCount . " wards:";
} else {
$wardsDescription = $config['councilName'] . " is unsubdivided and does not contain any wards.";
}
?>
<!-- wp:paragraph -->
<p><?php echo $wardsDescription; ?></p>
<!-- /wp:paragraph -->
<?php if (isset($media["map.jpg"])): ?>
<!-- wp:image {"id":<?php echo $media["map.jpg"]['id']; ?>,"width":"550px","sizeSlug":"full","linkDestination":"media","className":"is-style-default"} -->
<figure class="wp-block-image size-full is-resized is-style-default"><a href="<?php echo $media["map.jpg"]['url']; ?>" target="_blank" rel="noreferrer noopener"><img src="<?php echo $media["map.jpg"]['url']; ?>" alt="" class="wp-image-<?php echo $media["map.jpg"]['id']; ?>" style="width:550px"/></a></figure>
<!-- /wp:image -->
<?php endif ?>
<?php if ($wardCount > 1): ?>
<?php
if ($wardCount > 8) {
$wardListChunkSize = ceil($wardCount / 2);
} else {
$wardListChunkSize = $wardCount;
}
$wardChunks = array_chunk($config['wardNames'], $wardListChunkSize);
?>
<!-- wp:columns {"className":"ward-list-columns"} -->
<div class="wp-block-columns ward-list-columns">
<?php for ($columnIdx = 0; $columnIdx < 4; $columnIdx++): ?>
<!-- wp:column {"verticalAlignment":"top","style":{"spacing":{"padding":{"top":"0","bottom":"0"}}}} -->
<div class="wp-block-column is-vertically-aligned-top" style="padding-top:0;padding-bottom:0">
<?php if (array_key_exists($columnIdx, $wardChunks)): ?>
<!-- wp:list {"style":{"spacing":{"margin":{"top":"0","right":"0","bottom":"0","left":"0"}}}} -->
<ul style="margin-top:0;margin-right:0;margin-bottom:0;margin-left:0" class="wp-block-list">
<?php foreach($wardChunks[$columnIdx] as $wardName): ?>
<!-- wp:list-item -->
<li><a href="#<?php echo sluggify($wardName); ?>"><?php echo $wardName; ?></a></li>
<!-- /wp:list-item -->
<?php endforeach; ?>
</ul>
<!-- /wp:list -->
<?php endif; ?>
</div>
<!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php else: ?>
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->
<?php endif; ?>
<?php
if (isset($config['groupNames'])) {
$groupNames = $config['groupNames'];
$propertyOnCandidate = 'Group';
} else {
$groupNames = $config['wardNames'];
$propertyOnCandidate = 'Ward';
}
?>
<?php foreach ($groupNames as $index => $groupName): ?>
<!-- wp:heading {"level":3,"className":"is-style-default"} --> <!-- wp:heading {"level":3,"className":"is-style-default"} -->
<?php $wardSlug = strtolower(str_replace(' ', '-', $wardName)); ?> <?php $groupSlug = sluggify($groupName); ?>
<h3 class="wp-block-heading is-style-default" id="<?php echo $wardSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $wardSlug; ?>"><?php echo $wardName; ?></a></h3> <h3 class="wp-block-heading is-style-default" id="<?php echo $groupSlug; ?>"><a style="text-decoration: none;" href="#<?php echo $groupSlug; ?>"><?php echo htmlspecialchars($groupName); ?></a></h3>
<!-- /wp:heading --> <!-- /wp:heading -->
<?php <?php
$wardCandidates = array_filter($candidates, function ($candidate) use ($wardName) { $groupCandidates = array_filter($candidates, function ($candidate) use ($groupName, $propertyOnCandidate) {
return isset($candidate["Ward"]) && $candidate["Ward"] === $wardName; return isset($candidate[$propertyOnCandidate]) &&
strtolower($candidate[$propertyOnCandidate]) === strtolower($groupName);
}); });
usort($wardCandidates, function($a, $b) { usort($groupCandidates, function($a, $b) {
if ($a == $b) { if ($a == $b) {
return 0; return 0;
} }
return (((int) $a) < ((int) $b)) ? 1 : -1; return (((int) $a['Rating']) < ((int) $b['Rating'])) ? 1 : -1;
}); });
if (count($wardCandidates) == 0) continue; if (count($groupCandidates) > 0):
?> ?>
<!-- wp:group {"style":{"spacing":{"padding":{"top":"0","bottom":"7rem"}}},"layout":{"type":"grid","columnCount":4}} -->
<div class="wp-block-group" style="padding-top:0;padding-bottom:7rem">
<?php foreach ($wardCandidates as $index => $candidate): ?>
<!-- wp:group {"layout":{"type":"flex","orientation":"vertical","justifyContent":"center"}} -->
<div class="wp-block-group">
<?php <?php
$columnCount = 4;
$chunkedWardCandidates = array_chunk($groupCandidates, $columnCount);
?>
<?php foreach($chunkedWardCandidates as $chunk): ?>
<!-- wp:columns -->
<div class="wp-block-columns">
<?php for ($columnIdx = 0; $columnIdx < $columnCount; $columnIdx++): ?>
<!-- wp:column -->
<div class="wp-block-column">
<?php if (array_key_exists($columnIdx, $chunk)): ?>
<?php
$candidate = $chunk[$columnIdx];
if (isset($candidate["Elected"]) && $candidate["Elected"]) {
$candidate_elected = true;
} else {
$candidate_elected = false;
}
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) { if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
$candidate_image = $media[$candidate['Picture']]; $candidate_image = $media[$candidate['Picture']];
} else { } else {
$candidate_image = $media['default.png']; $candidate_image = $media['default.png'];
} }
$candidate_rating = str_repeat("✔️", max(0, min(5, $candidate['Rating'])));
// If string is 5 ticks, insert a zero width space entity between the 3rd and 4th ticks so that it wraps nicer
if ($candidate_rating == "✔️✔️✔️✔️✔️") {
$candidate_rating = "✔️✔️✔️&#8203;✔️✔️";
}
// If string is 4 ticks, insert a zero width space entity between the 2nd and 3rd ticks so that it wraps nicer
if ($candidate_rating == "✔️✔️✔️✔️") {
$candidate_rating = "✔️✔️&#8203;✔️✔️";
}
?> ?>
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","style":{"color":{}},"className":"is-resized"} --> <!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","align":"center","style":{"color":{}},"className":"is-resized"} -->
<figure class="wp-block-image is-resized"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px"/></figure> <figure class="wp-block-image aligncenter is-resized <?php if ($candidate_elected) { echo "elected-candidate"; } ?>"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px;"/>
<?php if ($candidate_elected): ?>
<figcaption>ELECTED</figcaption>
<?php endif; ?>
</figure>
<!-- /wp:image --> <!-- /wp:image -->
<!-- wp:heading {"fontSize":"medium"} --> <!-- wp:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->
<h2 class="wp-block-heading has-medium-font-size"><strong><?php echo $candidate['Candidate Name']; ?></strong></h2> <h2 class="wp-block-heading has-text-align-center has-medium-font-size" style="margin-top:1rem;margin-bottom:0.5rem"><strong><?php echo htmlspecialchars($candidate['Candidate Name']); ?></strong></h2>
<!-- /wp:heading --> <!-- /wp:heading -->
<!-- wp:paragraph {"style":{"layout":{"selfStretch":"fit","flexSize":null}},"fontSize":"large"} --> <!-- wp:paragraph {"align":"center","style":{"layout":{"selfStretch":"fit","flexSize":null},"typography":{"lineHeight":"1"},"spacing":{"margin":{"top":"0.5rem","bottom":"1.5rem"}}},"fontSize":"large"} -->
<p class="has-large-font-size" style="color: rgba(100%, 0%, 0%, 0); text-shadow: 0 0 0 green;"><?php echo str_repeat("✔️", $candidate['Rating']); ?></p> <p class="has-text-align-center has-large-font-size candidate-ticks"><?php echo $candidate_rating; ?></p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
</div> <?php endif; ?>
<!-- /wp:group -->
<?php endforeach; ?>
</div> </div>
<!-- /wp:group --> <!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php endforeach; ?>
<?php else: ?>
<!-- wp:paragraph -->
<p>No candidates in this ward have completed the survey. Send your local candidates the <?php echo $surveyLink; ?> and ask them to take part so that local residents can vote for the candidates who want to build the streets people love.</p>
<!-- /wp:paragraph -->
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (isset($config['footer'])): ?> <?php if (isset($config['footer'])): ?>

View File

@@ -0,0 +1,42 @@
<?php
require_once("parse_pledge_data.php");
$options = getopt("", ["candidates-files:"]);
if (isset($options['candidates-files'])) {
$candidates_files = $options['candidates-files'];
} else {
error_log("Error: Missing required option '--candidates-files'.");
exit(1);
}
$candidate_data = parse_pledge_data(explode(" ", $candidates_files), null);
/* Select people who have taken the pledge (and have an image) */
$pledgeCandidates = array_filter($candidate_data, function ($candidate) {
return $candidate['Pledge'] === 'y' && $candidate['Picture'] !== "";
});
/* Select 9 random candidates */
$pledgeKeys = array_rand($pledgeCandidates, 9);
shuffle($pledgeKeys);
$i = 0;
foreach ($pledgeKeys as $key) {
$image_url = $pledgeCandidates[$key]['image_url'];
$image_id = $pledgeCandidates[$key]['image_id'];
echo "s|pledge_img_".$i."|".$image_url."|\n";
echo "s|pledge_id_".$i."|".$image_id."|\n";
echo "s|pledge_string_".$i."|";
echo $pledgeCandidates[$key]['Candidate Name'].
" (".
$pledgeCandidates[$key]['Council'].
") has taken the pledge!|\n";
$i++;
}
exit(0);

View File

@@ -0,0 +1,28 @@
<?php
class SPLPageRenderer {
public function renderPledgePage($councils, $lga_pages, $candidates) {
ob_start();
$didError = false;
set_error_handler(function($errno, $errstr, $errfile, $errline) use(&$didError) {
$didError = true;
error_log("Error: $errstr in $errfile on line $errline");
return true; // Prevent default error handling
});
require "template.php";
restore_error_handler();
$content = ob_get_clean();
// Explictly return null if we didn't generate any content or if there was an error
if (!empty($content) && !$didError) {
return $content;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
function trim_sluggify($input) {
return strtolower(str_replace(' ', '-', trim($input)));
}
function parse_pledge_data($candidates_files, $default_image) {
$candidate_data = [];
foreach ($candidates_files as $key => $file) {
$config_file = dirname($file)."/config.json";
$config_string = file_get_contents($config_file);
$elected_file = dirname($file)."/candidates-elected.csv";
if ($config_string !== FALSE) {
$config = json_decode($config_string, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
$elected_data = [];
if (file_exists($elected_file)) {
if (($elected_handle = fopen($elected_file, "r")) !== FALSE) {
$headers = fgetcsv($elected_handle);
while (($data = fgetcsv($elected_handle)) !== FALSE) {
$candidate = [];
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$name_slug = trim_sluggify($candidate['Candidate Name']);
$elected_data[$name_slug] = $candidate;
}
} else {
error_log('Error opening candidates file');
exit(1);
}
}
if (($handle = fopen($file, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
$candidate['Pledge'] = 'n';
$candidate['Picture'] = "";
if (is_array($default_image)) {
$candidate['image_url'] = $default_image['url'];
$candidate['image_id'] = $default_image['id'];
} else {
$candidate['image_url'] = "";
$candidate['image_id'] = "";
}
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$name_slug = trim_sluggify($candidate['Candidate Name']);
if (array_key_exists($name_slug, $candidate_data)) {
if ($candidate_data[$name_slug]['Pledge'] === 'y') {
$candidate['Pledge'] = 'y';
}
}
if (!empty($elected_data) && array_key_exists($name_slug, $elected_data)) {
$candidate['Elected'] = $elected_data[$name_slug]['Elected'];
}
$candidate['Council'] = $config['councilName'];
$candidate['Path'] = dirname($file);
$media_desc = $candidate['Path']."/".
$candidate['Picture'].".json";
if (file_exists($media_desc)) {
$media_string = file_get_contents($media_desc);
if ($media_string !== FALSE) {
$media = json_decode($media_string, true);
} else {
error_log("Error opening image descriptor.");
exit(1);
}
/* Get photo URL and ID */
$candidate['image_url'] = $media['url'];
$candidate['image_id'] = $media['id'];
}
$candidate_data[$name_slug] = $candidate;
}
fclose($handle);
} else {
error_log('Error opening candidates file');
exit(1);
}
/* Override pledge columns if pledges.csv is present */
$pledges_file = dirname($file)."/pledges.csv";
if (!file_exists($pledges_file)) continue;
if (($handle = fopen($pledges_file, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$candidate_data[trim_sluggify($candidate['Candidate Name'])]['Pledge'] =
$candidate['Pledge'];
}
fclose($handle);
} else {
error_log('Error opening pledges file');
exit(1);
}
}
return $candidate_data;
}

View File

@@ -0,0 +1,64 @@
<?php
require_once("parse_pledge_data.php");
require_once("page_renderer.php");
$options = getopt("", ["candidates-files:", "default-image:"]);
if (isset($options['candidates-files'])) {
$candidates_files = $options['candidates-files'];
} else {
error_log("Error: Missing required option '--candidates-files'.");
exit(1);
}
if (isset($options['default-image'])) {
$default_image = $options['default-image'];
} else {
error_log("Error: Missing required option '--default-image'.");
exit(1);
}
$default_image = file_get_contents($default_image);
if ($default_image !== FALSE) {
$default_image = json_decode($default_image, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
$candidate_data = parse_pledge_data(explode(" ", $candidates_files), $default_image);
/* Select people who have taken the pledge */
$pledgeCandidates = array_filter($candidate_data, function ($candidate) {
return $candidate['Pledge'] === 'y';
});
$renderer = new SPLPageRenderer();
//print_r($pledgeCandidates);
$councils = [];
$lga_pages_unsort = [];
foreach ($pledgeCandidates as $key => $candidate) {
$councils[] = $candidate['Council'];
$lga_pages_unsort[] = basename($candidate['Path']);
}
$councils = array_unique($councils);
asort($councils);
$lga_pages = [];
foreach ($councils as $key => $council) {
$lga_pages[$key] = $lga_pages_unsort[$key];
}
//print_r($councils);
$pageContent = $renderer->renderPledgePage($councils, $lga_pages, $pledgeCandidates);
if ($pageContent === null) {
exit(2);
}
echo $pageContent;
exit(0);

143
pledge-update/template.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
function sluggify($input) {
return strtolower(str_replace(' ', '-', $input));
}
?>
<!-- wp:paragraph -->
<p>The Streets People Love campaign offers council candidates the opportunity to take the following pledge:</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"style":{"color":{"background":"#10B5B0"}}} -->
<p class="has-background" style="background-color:#10B5B0">If elected Councillor, I pledge to allocate budget and street space to build streets people love, and ensure that residents of all ages and abilities can safely move around our council area, irrespective of whether they choose to walk, cycle, wheel, use public transport or drive.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Candidates from these councils have taken the pledge:</p>
<!-- /wp:paragraph -->
<?php
$councilCount = count($councils);
?>
<?php if ($councilCount > 1): ?>
<?php
if ($councilCount > 8) {
$councilListChunkSize = ceil($councilCount / 2);
} else {
$councilListChunkSize = $councilCount;
}
$councilChunks = array_chunk($councils, $councilListChunkSize);
?>
<!-- wp:columns {"className":"council-list-columns"} -->
<div class="wp-block-columns council-list-columns">
<?php for ($columnIdx = 0; $columnIdx < 4; $columnIdx++): ?>
<!-- wp:column {"verticalAlignment":"top","style":{"spacing":{"padding":{"top":"0","bottom":"0"}}}} -->
<div class="wp-block-column is-vertically-aligned-top" style="padding-top:0;padding-bottom:0">
<?php if (array_key_exists($columnIdx, $councilChunks)): ?>
<!-- wp:list {"style":{"spacing":{"margin":{"top":"0","right":"0","bottom":"0","left":"0"}}}} -->
<ul style="margin-top:0;margin-right:0;margin-bottom:0;margin-left:0" class="wp-block-list">
<?php foreach($councilChunks[$columnIdx] as $councilName): ?>
<!-- wp:list-item -->
<li><a href="#<?php echo sluggify($councilName); ?>"><?php echo $councilName; ?></a></li>
<!-- /wp:list-item -->
<?php endforeach; ?>
</ul>
<!-- /wp:list -->
<?php endif; ?>
</div>
<!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php else: ?>
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->
<?php endif; ?>
<?php foreach ($councils as $key => $council): ?>
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
<?php $groupSlug = sluggify($council); ?>
<h3 class="wp-block-heading is-style-default" id="<?php echo $groupSlug; ?>"><a style="text-decoration: none;" href="/<?php echo $lga_pages[$key]; ?>"><?php echo htmlspecialchars($council); ?></a></h3>
<!-- /wp:heading -->
<?php
$groupCandidates = array_filter($candidates, function ($candidate) use ($council) {
return $candidate['Council'] === $council;
});
?>
<?php
$columnCount = 4;
$chunkedCouncilCandidates = array_chunk($groupCandidates, $columnCount);
?>
<?php foreach($chunkedCouncilCandidates as $chunk): ?>
<!-- wp:columns -->
<div class="wp-block-columns">
<?php for ($columnIdx = 0; $columnIdx < $columnCount; $columnIdx++): ?>
<!-- wp:column -->
<div class="wp-block-column">
<?php if (array_key_exists($columnIdx, $chunk)): ?>
<?php
$candidate = $chunk[$columnIdx];
if (isset($candidate['Elected']) && $candidate['Elected'] === 'y') {
$candidate_elected = true;
} else {
$candidate_elected = false;
}
if (isset($candidate['Picture']) && strlen($candidate['image_url'])) {
$candidate_image['url'] = $candidate['image_url'];
$candidate_image['id'] = $candidate['image_id'];
} else {
continue;
}
?>
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","align":"center","style":{"color":{}},"className":"is-resized"} -->
<figure class="wp-block-image aligncenter is-resized <?php if ($candidate_elected) { echo "elected-candidate"; } ?>"><img src="<?php echo $candidate_image['url']; ?>" alt="" class="wp-image-<?php echo $candidate_image['id']; ?>" style="object-fit:cover;width:200px;height:200px;"/>
<?php if ($candidate_elected): ?>
<figcaption>ELECTED</figcaption>
<?php endif; ?>
</figure>
<!-- /wp:image -->
<!-- wp:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->
<h2 class="wp-block-heading has-text-align-center has-medium-font-size" style="margin-top:1rem;margin-bottom:0.5rem"><strong><?php echo htmlspecialchars($candidate['Candidate Name']); ?></strong></h2>
<!-- /wp:heading -->
<?php endif; ?>
</div>
<!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php endforeach; ?>
<?php endforeach; ?>
<?php if (isset($config['footer'])): ?>
<!-- wp:paragraph -->
<p><?php echo $config['footer']; ?></p>
<!-- /wp:paragraph -->
<?php endif; ?>

15
results/fetch.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
mkdir -p html
wget https://www.vec.vic.gov.au/results/2024-council-election-results -O html/lga_list.html
IFS=$'\n'
lgas=$(grep 'href="/voting/.*/results"' html/lga_list.html)
for lga in $lgas ; do
lga=$(sed 's|.*href="|https://www.vec.vic.gov.au|' <<< $lga)
lga=$(sed 's|">.*||' <<< $lga)
file=$(sed 's|.*elections/||' <<< $lga | sed s'|/results||')
wget $lga -O html/$file
done

142
results/gen-elected.php Normal file
View File

@@ -0,0 +1,142 @@
<?php
$options = getopt("", ["candidates-files:", "results-file:"]);
if (isset($options['candidates-files'])) {
$candidates_files = $options['candidates-files'];
} else {
error_log("Error: Missing required option '--candidates-files'.");
exit(1);
}
if (isset($options['results-file'])) {
$results_file = $options['results-file'];
$results_string = file_get_contents($results_file);
if ($results_string !== FALSE) {
$results = json_decode($results_string, true);
} else {
error_log("Error opening results.json.");
exit(1);
}
} else {
error_log("Error: Missing required option '--results-file'.");
exit(1);
}
function trim_sluggify($input) {
return strtolower(str_replace(' ', '-', trim($input)));
}
function match_words($words, $list) {
/* Match database names to VEC names */
$max_score = 0;
$best_match = "no match";
foreach ($list as $possible_match) {
$aa = preg_split("/[^a-z]/", strtolower($words));
$bb = preg_split("/[^a-z]/", strtolower($possible_match));
$score_sum = 0;
foreach ($aa as $a) {
foreach ($bb as $b) {
similar_text($a, $b, $score);
if ($score > 70) $score_sum += $score;
else $score_sum -= 1;
}
}
if ($score_sum > $max_score) {
$max_score = $score_sum;
$best_match = $possible_match;
}
}
return array($max_score, $best_match);
}
$candidates_files = explode(" ", $candidates_files);
/* Generate dictionary of candidates and LGAs */
$candidate_data = [];
foreach ($candidates_files as $file) {
$config_file = dirname($file)."/config.json";
$config_string = file_get_contents($config_file);
if ($config_string !== FALSE) {
$config = json_decode($config_string, true);
} else {
error_log("Error opening config.json.");
exit(1);
}
$candidate_data[$config['councilName']]['_filename'] = $file;
if (($handle = fopen($file, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$name_slug = trim_sluggify($candidate['Candidate Name']);
$candidate_data[$config['councilName']][$name_slug] = $candidate;
}
}
}
$vec_lga_names = [];
foreach ($results as $lga => $data) {
$vec_lga_names[] = $lga;
}
function was_elected($candidate, $vec_wards) {
foreach ($vec_wards as $vec_candidates) {
list($score, $match) = match_words($candidate, $vec_candidates);
if ($score > 180) return true;
}
return false;
}
$header = ["Ward", "Candidate Name", "Elected"];
foreach ($candidate_data as $lga => $db_candidates) {
/* Find LGA in results dict */
list($score, $vec_lga_name) = match_words($lga, $vec_lga_names);
$vec_wards = $results[$vec_lga_name];
$elected = [];
/* Go through database candidates and build list of elected candidates */
foreach ($db_candidates as $key => $value) {
if ($key === '_filename') {
$output_file = dirname($value)."/candidates-elected.csv";
continue;
}
if (was_elected($value['Candidate Name'], $vec_wards)) {
$elected[] = $value;
}
}
/* Don't create file if none were elected. */
if (count($elected) === 0) continue;
if (($handle = fopen($output_file, "w")) === FALSE) {
error_log('Error opening output file');
exit(1);
}
if (fputcsv($handle, $header) === FALSE) {
error_log('Error writing headers to output file');
exit(3);
}
foreach ($elected as $candidate) {
$line = array($candidate['Ward'], $candidate['Candidate Name'], "y");
if (fputcsv($handle, $line) === FALSE) {
error_log('Error writing candidate to output file');
exit(3);
}
}
fclose($handle);
}
exit(0);

53
results/parser.py Normal file
View File

@@ -0,0 +1,53 @@
from bs4 import BeautifulSoup, Tag as HTMLTag
import json, re, argparse
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
args = parser.parse_args()
def get_vacancies(ward):
text = ward.parent.parent.h2.text
ward_name = re.search("[^\(]*", text)[0].strip()
vacancies = int(re.search("\([0-9]+", text)[0].strip("("))
return (ward_name, vacancies, ward)
def get_candidate_names(ward_desc):
names = []
for sibling in ward_desc[2].parent.next_siblings:
if not isinstance(sibling, HTMLTag):
continue
if not (blocks := sibling.find_all('td', class_="list-item-body")):
continue
for block in blocks:
names.append(re.sub('\n.*', '', block.text.strip()))
return names
def parse_lga(filename):
with open(filename, 'r') as results_fp:
html_doc = results_fp.read()
soup = BeautifulSoup(html_doc, 'html.parser')
wards0 = soup.find_all(string="Successful candidates")
wards1 = soup.find_all(string="Elected candidates")
ward_info = []
for ward in wards0:
ward_info.append(get_vacancies(ward))
for ward in wards1:
ward_info.append(get_vacancies(ward))
results = {}
for ward in ward_info:
names = get_candidate_names(ward)
assert len(names) == ward[1]
results[ward[0]] = names
return results
all_results = {}
for lga in args.filenames:
lga_name = re.sub('html/lgas/', '', lga)
results = parse_lga(lga)
all_results[lga_name] = results
print(json.dumps(all_results, indent=4))

22
update-elected.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# This script uses the jq, wp, and php commands, make sure they are installed before running this script.
# The folder containing data for each council.
# Includes the list of candidates and any media.
DATA_PATH="../spl-data"
# Iterate over folders in data path
candidates_files=()
for folder in "$DATA_PATH"/*; do
if test -f "$folder"/candidates-generic.csv; then
candidates_files+=("$folder"/candidates-generic.csv)
fi
# Community groups get priority
if test -f "$folder"/candidates.csv; then
candidates_files+=("$folder"/candidates.csv)
fi
done
php results/gen-elected.php --candidates-files "${candidates_files[*]}" \
--results-file $DATA_PATH/results.json

31
update-pledges.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# If this runs as a cron job - might want to limit the number of revisions
# wordpress stores:
# wp-config.php:
# define( 'WP_POST_REVISIONS', 3 );
#wp post list --post_type=page
#wp post get 426 --field=content > current-homepage
#wp post get 1409 --field=content > movie-homepage
#wp post create --post_type=page --post_title="test_pledge" movie-homepage
#wp post update 1803 ../spl-data/movie-homepage
DATA_PATH="../spl-data"
candidates_files=()
for folder in "$DATA_PATH"/*; do
if test -f "$folder"/candidates-generic.csv; then
candidates_files+=("$folder"/candidates-generic.csv)
fi
# Community groups get priority
if test -f "$folder"/candidates.csv; then
candidates_files+=("$folder"/candidates.csv)
fi
done
pledge_sed=$(php pledge-update/homepage.php --candidates-files "${candidates_files[*]}")
content=$(sed "$pledge_sed" ../spl-data/movie-homepage)
echo "$content" | wp post update 1803 -

2
upload-media.sh Normal file → Executable file
View File

@@ -7,7 +7,7 @@
# Additionally, make sure the wp-cli/restful package is installed in the wp command (via "wp package install wp-cli/restful") # Additionally, make sure the wp-cli/restful package is installed in the wp command (via "wp package install wp-cli/restful")
# Controls the flags that are passed to every usage of the wp command. # Controls the flags that are passed to every usage of the wp command.
WP_FLAGS="--allow-root --path=/var/www/html" #WP_FLAGS="--allow-root --path=/var/www/html"
media_path="$1" media_path="$1"