Compare commits

...

133 Commits

Author SHA1 Message Date
35812791f5 Add text to top of Federal Divisions page 2025-04-24 17:45:26 +10:00
72c7413195 Use convert command if magick command isn't installed 2025-04-21 21:40:40 +10:00
Kim Taylor
2cf3d0bd26 Ignore candidates from divisions outside Victoria. 2025-04-19 15:47:49 +10:00
Kim Taylor
babf8f81b6 Pledge page specifically mentions question 4. 2025-04-18 11:00:42 +10:00
Kim Taylor
1e3e4a99e6 Update form link on division pages. 2025-04-14 21:22:23 +10:00
Kim Taylor
60a33194e1 Update pledge-page generation for federal election. 2025-04-13 15:57:04 +10:00
Kim Taylor
d8684760e5 Update homepage pledge image rotation for federal. 2025-04-13 13:36:50 +10:00
Kim Taylor
1735d52781 Update division page generation. 2025-04-11 21:21:42 +10:00
Kim Taylor
675058201a Update date generic form parser for federal 2025 form. 2025-03-30 23:02:48 +11:00
Kim Taylor
05b0898979 Only one election. 2025-03-04 23:53:49 +11:00
Kim Taylor
c172222e15 Update wording on division pages. 2025-03-04 23:51:10 +11:00
Kim Taylor
5e3d22de3e Update front page auto-update to look at federal data. 2025-03-04 22:42:50 +11:00
Kim Taylor
9b9c1a6869 Update pledge page to generate pledges for federal divisions. 2025-03-04 22:28:19 +11:00
Kim Taylor
f5f04d01eb Update LGA page to generate Federal Divisions page. 2025-03-04 21:58:26 +11:00
Kim Taylor
0bddcb7520 Generate division pages. Need to update text. 2025-03-04 21:44:12 +11:00
Kim Taylor
608ed0f92c Update map generator for federal election. 2025-03-03 21:05:24 +11:00
Kim Taylor
2a0ba8eed2 Update pledge message for federal election. 2025-03-02 15:54:33 +11:00
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
43 changed files with 1921 additions and 203 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.
## 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
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:
```
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
@@ -20,4 +52,9 @@ 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.
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"
wp package install wp-cli/restful $WP_FLAGS
if test -d "$path"; then
echo "Found $path, starting upload."
for file in "$path"/*.jpg; do
for file in "$path"/*/*.jpg; do
./upload-media.sh "$file"
done
for file in "$path"/*.jpeg; do
for file in "$path"/*/*.jpeg; do
./upload-media.sh "$file"
done
for file in "$path"/*.png; do
for file in "$path"/*/*.png; do
./upload-media.sh "$file"
done
for file in "$path"/*.gif; do
for file in "$path"/*/*.gif; do
./upload-media.sh "$file"
done
else

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

View File

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

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

@@ -0,0 +1,145 @@
<?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['q0'] === "Yes") $score++;
if ($candidate['q1'] === "Yes") $score++;
if ($candidate['q2'] === "Yes") $score++;
if ($candidate['q3'] === "Yes") $score++;
if ($candidate['q4'] === "Yes") $score++;
$candidate_data[$key]['Score'] = $score;
$candidate_data[$key]['Pledge'] = $candidate['q4'];
}
$header = ["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_division'] === $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['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_division'] = $candidate['match_division'];
$image_map[$candidate['Photo']] = $map;
}
}
$json_data = json_encode($image_map);
print_r($json_data);
exit(0);

View File

@@ -0,0 +1,106 @@
<?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 (strstr($value, "candidate photo")) $value = "Photo";
if (strstr($value, "In which federal Division are")) continue;
if (strstr($value, "In which Federal Division are")) $value = "Division";
if (strstr($value, "Political Party")) $value = "Party";
if (strstr($value, "Protected bike lanes provide")) continue;
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 === "Party") {
$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;
$match_lga = null;
foreach ($lga_list as $lga) {
$aa = preg_split("/[^a-z]/", strtolower($candidate['Division']));
$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;
}
}
*/
if ($match_lga === null) {
$candidate['match_division'] = "no_match";
} else {
$candidate['match_division'] = $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",
"onAutoForward": "notify"
}
}
},
// 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"
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
"mounts": [
"source=${localWorkspaceFolder}/../../spl-data,target=/workspaces/spl-data,type=bind,consistency=consistent"
]
}

View File

@@ -1,5 +1,31 @@
<?php
$options = getopt("", ["input:", "output:", "media:"]);
$options = getopt("", ["folder:", "input:", "output:", "media:"]);
if (isset($options['folder'])) {
$folder = $options['folder'];
if (is_dir($folder)) {
$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 {
error_log("Error: Specified folder is not valid.");
exit(1);
}
}
if (isset($options['input'])) {
$inputFile = $options['input'];
@@ -51,50 +77,91 @@ if (($handle = fopen($inputFile, "r")) !== FALSE) {
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 ($data[0] == "Candidate") {
if (trim($data[0]) == "Candidate" || trim($data[0]) == "") {
if ($currentWard == null) {
error_log("No ward found, skipping data on line " . $currentLine);
continue;
}
$candidateName = $data[1];
$candidateName = trim($data[1]);
if ($candidateName == " example name") {
if ($candidateName == "example name" || $candidateName == "") {
error_log("Skipping line ". $currentLine);
continue;
}
print("Adding candidate " . $candidateName . " to ". $currentWard . "\n");
//print("Adding candidate " . $candidateName . " to ". $currentWard . "\n");
$name_split = explode(" ", $data[1]);
$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)),
".*" . implode(".*", $name_split) . ".*",
".*" . implode(".*", array_reverse($name_split)) . ".*"
];
$regex_groups = array_map(function($x) { return "(?:.*" . $x . ".*)"; }, $name_patterns);
$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("Failed to identify picture for " . $candidateName);
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" => $data[2],
"Rating" => $rating,
"Picture" => $picture
]
);
@@ -139,6 +206,6 @@ if (($handle = fopen($outputFile, "w")) !== FALSE) {
exit(1);
}
print("Data written to " . $outputFile);
print("Data written to " . $outputFile . "\n");
exit(0);

71
get-generic.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/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/federal_2025"
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_federal_2025/Streets People Love Federal Election candidate pledge (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_division\"]" <<< $image_map)
if [ ! -d $"$DATA_PATH/$lga" ] ; then
continue
fi
dst="$DATA_PATH/$lga/$key$suffix"
if [ -f $dst ] ; then
continue
fi
echo "Resizing $dst"
if command -v magick >/dev/null 2>&1; then
magick $IMAGES/$key -resize 400x400 $dst
else
convert $IMAGES/$key -resize 400x400 $dst
fi
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>") | .[]

57
lga-page.php Normal file
View File

@@ -0,0 +1,57 @@
<?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:paragraph -->' . "\n");
print('<p>The Streets People Love campaign has created scorecards for candidates in the 2025 federal election. 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 division in which they are running.</p>' . "\n");
print('<!-- /wp:paragraph -->' . "\n");
print('<!-- wp:paragraph -->' . "\n");
print('<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 <a href="https://forms.gle/CeGbJF11SUkATjfN8">Streets People Love Pledge and Survey</a> 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>' . "\n");
print('<!-- /wp:paragraph -->' . "\n");
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['divisionName'] . "</a></li>\n");
print('<!-- /wp:list-item -->' . "\n");
}
print('</ul> <!-- /wp:list -->');
exit(0);

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

@@ -4,10 +4,10 @@
# The folder containing data for each council.
# Includes the list of candidates and any media.
DATA_PATH="../spl-data"
DATA_PATH="../spl-data/federal_2025"
# 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() {
local council_block="$1"
@@ -23,7 +23,7 @@ function create_or_update_page() {
media_inputs+=("$file")
fi
done
for file in "$DATA_PATH"/*.{jpeg,jpg,png,gif}.json; do
for file in "$DATA_PATH"/../*.{jpeg,jpg,png,gif}.json; do
if test -f "$file"; then
media_inputs+=("$file")
fi
@@ -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
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
@@ -40,7 +53,7 @@ function create_or_update_page() {
echo "$content" | wp post update "$page_id" --post_content="$content" $WP_FLAGS -
else
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
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/federal_2025"
# 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 245813 -

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/federal_2025"
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 245816 -

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);
}
})();

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

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,8 @@
"packages": {
"": {
"dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"polylabel": "^2.0.1",
"puppeteer-core": "^23.1.0"
}
@@ -201,6 +203,11 @@
"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",
@@ -386,6 +393,11 @@
"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",

View File

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

View File

@@ -12,4 +12,6 @@ To automatically compile the `src.js` file after every edit, run `webpack-cli --
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-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.

View File

@@ -0,0 +1,11 @@
SHPFILE := ../../../spl-data/federal_2025/aec_data/E_VIC24_region.shp
SHAPELIB := ../../../lib/shapelib-1.6.0
../federal_2025_boundaries.json: $(SHPFILE) shptojson
LD_LIBRARY_PATH=$(SHAPELIB)/.libs ./shptojson $(SHPFILE) > $@
CFLAGS := -I $(SHAPELIB)
LDFLAGS := -L $(SHAPELIB)/.libs
shptojson: shptojson.c
gcc -Wall $(CFLAGS) $(LDFLAGS) -lshp $< -o $@

View File

@@ -0,0 +1,142 @@
/******************************************************************************
*
* Project: Shapelib
* Purpose: Sample application for dumping contents of a shapefile to
* the terminal in human readable form.
* Author: Frank Warmerdam, warmerdam@pobox.com
*
******************************************************************************
* Copyright (c) 1999, Frank Warmerdam
*
* SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
******************************************************************************
*
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shapefil.h"
int main(int argc, char **argv)
{
int nPrecision = 15;
int first;
const char *divison_name;
/* -------------------------------------------------------------------- */
/* Display a usage message. */
/* -------------------------------------------------------------------- */
if (argc != 2)
{
printf("Usage: shptojson shp_file\n");
exit(1);
}
/* -------------------------------------------------------------------- */
/* Open the passed shapefile. */
/* -------------------------------------------------------------------- */
SHPHandle hSHP = SHPOpen(argv[1], "rb");
DBFHandle hDBF = DBFOpen(argv[1], "rb");
if (hSHP == NULL || hDBF == NULL)
{
printf("Unable to open:%s\n", argv[1]);
exit(1);
}
/* -------------------------------------------------------------------- */
/* Print out the file bounds. */
/* -------------------------------------------------------------------- */
int nEntities;
int nShapeType;
double adfMinBound[4];
double adfMaxBound[4];
SHPGetInfo(hSHP, &nEntities, &nShapeType, adfMinBound, adfMaxBound);
fprintf(stderr, "Shapefile Type: %s # of Shapes: %d\n\n",
SHPTypeName(nShapeType), nEntities);
/* -------------------------------------------------------------------- */
/* Skim over the list of shapes, printing all the vertices. */
/* -------------------------------------------------------------------- */
printf("[\n");
for (int i = 0; i < nEntities; i++)
{
SHPObject *psShape = SHPReadObject(hSHP, i);
if (psShape == NULL)
{
fprintf(stderr,
"Unable to read shape %d, terminating object reading.\n",
i);
break;
}
divison_name = DBFReadStringAttribute(hDBF, i, 1);
fprintf(stderr, "shape %s has %i parts\n", divison_name, psShape->nParts);
printf(" {\n");
printf(" \"electorateId\": \"%08i\",\n", i);
printf(" \"electorateName\": \"%s\",\n", divison_name);
printf(" \"electorateType\": \"Division\",\n");
printf(" \"parentElectorateName\": \"%s\",\n", divison_name);
printf(" \"boundaryJson\": \"{\\\"type\\\": ");
if (psShape->nParts != 1)
{
printf("\\\"MultiPolygon\\\",\\\"coordinates\\\":[[[");
}
else
{
printf("\\\"Polygon\\\",\\\"coordinates\\\":[[");
}
first = 1;
for (int j = 0, iPart = 1; j < psShape->nVertices; j++)
{
if (iPart < psShape->nParts && psShape->panPartStart[iPart] == j)
{
iPart++;
printf("]],[[");
}
else if (!first)
{
printf(",");
}
first = 0;
printf("[%.*g,%.*g]", nPrecision, psShape->padfX[j],
nPrecision, psShape->padfY[j]);
}
if (i == (nEntities-1))
{
if (psShape->nParts != 1)
{
printf("]]]}\"\n }\n");
}
else
{
printf("]]}\"\n }\n");
}
}
else
{
if (psShape->nParts != 1)
{
printf("]]]}\"\n },\n");
}
else
{
printf("]]}\"\n },\n");
}
}
SHPDestroyObject(psShape);
}
printf("]\n");
SHPClose(hSHP);
DBFClose(hDBF);
exit(0);
}

View File

@@ -1,7 +1,7 @@
import polylabel from 'polylabel';
function normaliseCouncilName(str) {
const regex = /(.*?)(?:(?: Rural)?(?: City| Shire) Council)/g;
const regex = /(.*?)(?:(?: Rural)?(?: City| Shire| Borough) Council)/g;
const matches = str.matchAll(regex);
// If we get a match, convert to slug format
@@ -21,141 +21,176 @@ mapboxgl.accessToken = 'pk.eyJ1IjoibWF0dHl3YXkiLCJhIjoiY2x6eG9vMzZyMHY2cDJqb3M1O
const map = new mapboxgl.Map({
container: 'map',
zoom: 10,
style: 'mapbox://styles/mattyway/clzy2ozzf004k01pn840h9xdb',
style: 'mapbox://styles/mattyway/cm03vw57q00fd01pn93wf2j7p',
center: [145.00724,-37.79011]
});
fetch("wards_withboundaries.json")
.then(response => {
response.json()
.then((wardData) => {
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
map.on('load', () => {
// Load an image from an external URL.
map.loadImage('circle.png', (error, image) => {
if (error) throw error;
var bounds = {
"west": undefined,
"south": undefined,
"east": undefined,
"north": undefined
}
map.addImage('blue-circle', image);
var labelFeatures = [];
fetch("federal_2025_boundaries.json")
.then(response => {
response.json()
.then((wardData) => {
const filteredWardData = wardData.filter((ward) => normaliseCouncilName(ward.parentElectorateName) == councilName);
filteredWardData.forEach(wardData => {
const featureCollection = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': JSON.parse(wardData.boundaryJson)
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];
}
]
};
featureCollection.features[0].geometry.coordinates[0].forEach(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];
}
}
if (bounds.south == undefined || coordinate[1] < bounds.south) {
bounds.south = coordinate[1];
}
var labelFeatures = [];
if (bounds.east == undefined || coordinate[0] > bounds.east) {
bounds.east = coordinate[0];
}
filteredWardData.forEach(wardData => {
const featureCollection = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': JSON.parse(wardData.boundaryJson)
}
]
};
if (bounds.north == undefined || coordinate[1] > bounds.north) {
bounds.north = coordinate[1];
}
});
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 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
}
});
// 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
}
});
const centrePoint = polylabel(featureCollection.features[0].geometry.coordinates, 0.000001);
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[0], 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
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
}
});
}
});
} 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.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.addLayer({
'id': 'labels',
'type': 'symbol',
'source': 'labels',
'layout': {
'text-field': ['get', 'description'],
'text-variable-anchor': ['center', 'top', 'bottom'],
'text-radial-offset': 0.5,
'text-padding': 0,
'text-justify': 'auto',
'text-allow-overlap': false,
'text-ignore-placement': false,
}
});
map.fitBounds([
[bounds.west, bounds.south],
[bounds.east, bounds.north]
], {
padding: 25,
animate: false
});
map.fitBounds([
[bounds.west, bounds.south],
[bounds.east, bounds.north]
], {
padding: 25,
animate: false
});
}).catch(err => {
console.log(err);
}).catch(err => {
console.log(err);
});
});
});
});
});

View File

@@ -6,7 +6,7 @@
"type": "php",
"request": "launch",
"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}",
"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
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'])) {
$councilFileContents = file_get_contents($options['council-file']);
@@ -27,23 +27,23 @@ if (isset($options['candidates-file'])) {
// Convert CSV into an array of dictionaries. Use the header as the key in the dictionary.
$candidateData = [];
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
if (file_exists($candidatesFile)) {
if (($handle = fopen($candidatesFile, "r")) !== FALSE) {
$headers = fgetcsv($handle);
while (($data = fgetcsv($handle)) !== FALSE) {
$candidate = [];
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$candidateData[] = $candidate;
}
$candidateData[] = $candidate;
fclose($handle);
} else {
error_log('Error opening candidates file');
exit(1);
}
fclose($handle);
} else {
error_log('Error opening candidates file');
exit(1);
}
if (empty($candidateData)) {
error_log("Failed to load any candidates for " . $councilData['shortName']);
error_log("The specified candidates.csv file does not exist, will not show any candidates for " . $councilData["shortName"] . ".");
}
if (isset($options['media-file'])) {
@@ -55,6 +55,37 @@ if (isset($options['media-file'])) {
$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();
$pageContent = $renderer->renderCouncilPage($councilData, $candidateData, $mediaData);
if ($pageContent === null) {

View File

@@ -1,9 +1,30 @@
<!-- wp:paragraph {"align":"center"} -->
<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>
<?php
function sluggify($input) {
return strtolower(str_replace(' ', '-', $input));
}
$surveyLink = "<a href=\"https://forms.gle/CeGbJF11SUkATjfN8\">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 2025 federal election. 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 division in which they are running.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} -->
<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>
<!-- wp:paragraph -->
<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 -->
<?php if (isset($media["header.jpg"])): ?>
@@ -12,56 +33,172 @@
<!-- /wp:image -->
<?php endif ?>
<?php foreach ($config['wardNames'] as $index => $wardName): ?>
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
<?php $wardSlug = strtolower(str_replace(' ', '-', $wardName)); ?>
<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>
<!-- /wp:heading -->
<?php
$wardCount = 1; //count($config['wardNames']);
<?php
$wardCandidates = array_filter($candidates, function ($candidate) use ($wardName) {
return isset($candidate["Ward"]) && $candidate["Ward"] === $wardName;
});
if ($wardCount > 1) {
$wardsDescription = $config['divisionName'] . " is divided into " . $wardCount . " wards:";
} else {
$wardsDescription = $config['divisionName'] . " is unsubdivided and does not contain any wards.";
}
usort($wardCandidates, function($a, $b) {
if ($a == $b) {
return 0;
?>
<?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;
}
return (((int) $a) < ((int) $b)) ? 1 : -1;
});
if (count($wardCandidates) == 0) continue;
$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 = ['N/A']; // $config['wardNames'];
$propertyOnCandidate = 'Ward';
}
?>
<!-- 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
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
$candidate_image = $media[$candidate['Picture']];
} else {
$candidate_image = $media['default.png'];
}
<?php foreach ($groupNames as $index => $groupName): ?>
<?php
$groupCandidates = array_filter($candidates, function ($candidate) use ($groupName, $propertyOnCandidate) {
return isset($candidate[$propertyOnCandidate]) &&
strtolower($candidate[$propertyOnCandidate]) === strtolower($groupName);
});
usort($candidates, function($a, $b) {
if ($a == $b) {
return 0;
}
return (((int) $a['Rating']) < ((int) $b['Rating'])) ? 1 : -1;
});
if (count($candidates) > 0):
?>
<?php
$columnCount = 4;
$chunkedWardCandidates = array_chunk($candidates, $columnCount);
?>
<!-- wp:image {"id":<?php echo $candidate_image['id']; ?>,"width":"200px","height":"200px","scale":"cover","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>
<!-- /wp:image -->
<?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">
<!-- wp:heading {"fontSize":"medium"} -->
<h2 class="wp-block-heading has-medium-font-size"><strong><?php echo $candidate['Candidate Name']; ?></strong></h2>
<!-- /wp:heading -->
<?php if (array_key_exists($columnIdx, $chunk)): ?>
<?php
$candidate = $chunk[$columnIdx];
<!-- wp:paragraph {"style":{"layout":{"selfStretch":"fit","flexSize":null}},"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>
if (isset($candidate["Elected"]) && $candidate["Elected"]) {
$candidate_elected = true;
} else {
$candidate_elected = false;
}
if (isset($candidate['Picture']) && isset($media[$candidate['Picture']])) {
$candidate_image = $media[$candidate['Picture']];
} else {
$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","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 -->
<!-- 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-text-align-center has-large-font-size candidate-ticks"><?php echo $candidate_rating; ?></p>
<!-- /wp:paragraph -->
<?php endif; ?>
</div>
<!-- /wp:column -->
<?php endfor; ?>
</div>
<!-- /wp:columns -->
<?php endforeach; ?>
<?php else: ?>
<!-- wp:paragraph -->
<p>No candidates in this division 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 -->
</div>
<!-- /wp:group -->
<?php endforeach; ?>
</div>
<!-- /wp:group -->
<?php endif; ?>
<?php endforeach; ?>
<?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'] === 'Yes' && $candidate['Picture'] !== "";
});
/* Select 9 random candidates */
$pledgeKeys = array_rand($pledgeCandidates, min(count($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['divisionName'];
$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'] === 'Yes';
});
$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 federal 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, I pledge to vote for a national Active Transport Infrastructure Program that invests $400 million annually, equivalent to $15 per person, for the duration of the <a href="https://www.betterstreets.org.au/2025-federal-election">United Nations Decade of Sustainable Transport</a>, from 2026 to 2035.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Candidates from these divisions 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 $divisionName): ?>
<!-- wp:list-item -->
<li><a href="#<?php echo sluggify($divisionName); ?>"><?php echo $divisionName; ?></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/federal_2025"
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")
# 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"