Compare commits

...

64 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
e2fbd1b1ef Calculate scores based on Faith's criteria. 2024-09-21 19:49:49 +10:00
Kim Taylor
2453d550ce Fetch generic data from google. 2024-09-20 10:19:57 +10:00
29 changed files with 1083 additions and 66 deletions

View File

@@ -35,7 +35,7 @@ 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

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

@@ -77,9 +77,28 @@ if (($handle = fopen($inputFile, "r")) !== FALSE) {
if ($currentWard == "Coastal-promontory") {
$currentWard = "Coastal-Promontory";
}
if ($currentWard == "East Gippsland Shire") {
$currentWard = "Unsubdivided";
}
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) {
@@ -96,19 +115,31 @@ if (($handle = fopen($inputFile, "r")) !== FALSE) {
//print("Adding candidate " . $candidateName . " to ". $currentWard . "\n");
$name_split = explode(" ", str_replace(",", "", str_replace("'", "_", $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;

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[*]}"

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

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 -

View File

@@ -4,16 +4,22 @@
# 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"
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[*]}")
content=$(php pledge-update/pledge-page.php --candidates-files "${candidates_files[*]}" \
--default-image $DEFAULT_IMAGE)
wp post update 12106 --post_content="$content"
echo "$content" | wp post update 245816 -

File diff suppressed because one or more lines are too long

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
@@ -32,7 +32,7 @@ map.on('load', () => {
map.addImage('blue-circle', image);
fetch("wards_withboundaries.json")
fetch("federal_2025_boundaries.json")
.then(response => {
response.json()
.then((wardData) => {
@@ -117,7 +117,7 @@ map.on('load', () => {
//
// 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);
centrePoint = polylabel(featureCollection.features[0].geometry.coordinates[0], 0.000001);
}

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

@@ -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']);
@@ -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

@@ -4,7 +4,7 @@ function sluggify($input) {
return strtolower(str_replace(' ', '-', $input));
}
$surveyLink = "<a href=\"https://forms.gle/gnDNyBiVC64tDo2Y7\">Streets People Love Pledge and Survey</a>";
$surveyLink = "<a href=\"https://forms.gle/CeGbJF11SUkATjfN8\">Streets People Love Pledge and Survey</a>";
if (isset($config["survey"])) {
$surveyLink = $config["survey"];
}
@@ -12,11 +12,19 @@ if (isset($config["survey"])) {
?>
<!-- wp:paragraph -->
<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>
<?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 -->
<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"])): ?>
@@ -27,20 +35,16 @@ if (isset($config["survey"])) {
<?php
$wardCount = count($config['wardNames']);
$wardCount = 1; //count($config['wardNames']);
if ($wardCount > 1) {
$wardsDescription = $config['councilName'] . " is divided into " . $wardCount . " wards:";
$wardsDescription = $config['divisionName'] . " is divided into " . $wardCount . " wards:";
} else {
$wardsDescription = $config['councilName'] . " is unsubdivided and does not contain any wards.";
$wardsDescription = $config['divisionName'] . " 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>
@@ -98,37 +102,32 @@ if (isset($config["survey"])) {
$groupNames = $config['groupNames'];
$propertyOnCandidate = 'Group';
} else {
$groupNames = $config['wardNames'];
$groupNames = ['N/A']; // $config['wardNames'];
$propertyOnCandidate = 'Ward';
}
?>
<?php foreach ($groupNames as $index => $groupName): ?>
<!-- wp:heading {"level":3,"className":"is-style-default"} -->
<?php $groupSlug = sluggify($groupName); ?>
<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 -->
<?php
$groupCandidates = array_filter($candidates, function ($candidate) use ($groupName, $propertyOnCandidate) {
return isset($candidate[$propertyOnCandidate]) &&
strtolower($candidate[$propertyOnCandidate]) === strtolower($groupName);
});
usort($groupCandidates, function($a, $b) {
usort($candidates, function($a, $b) {
if ($a == $b) {
return 0;
}
return (((int) $a['Rating']) < ((int) $b['Rating'])) ? 1 : -1;
});
if (count($groupCandidates) > 0):
if (count($candidates) > 0):
?>
<?php
$columnCount = 4;
$chunkedWardCandidates = array_chunk($groupCandidates, $columnCount);
$chunkedWardCandidates = array_chunk($candidates, $columnCount);
?>
<?php foreach($chunkedWardCandidates as $chunk): ?>
@@ -143,6 +142,12 @@ if (isset($config["survey"])) {
<?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']])) {
$candidate_image = $media[$candidate['Picture']];
} else {
@@ -162,7 +167,11 @@ if (isset($config["survey"])) {
?>
<!-- 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"><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:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->
@@ -185,7 +194,7 @@ if (isset($config["survey"])) {
<?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>
<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 -->
<?php endif; ?>

View File

@@ -11,15 +11,15 @@ if (isset($options['candidates-files'])) {
exit(1);
}
$candidate_data = parse_pledge_data(explode(" ", $candidates_files));
$candidate_data = parse_pledge_data(explode(" ", $candidates_files), null);
/* Select people who have taken the pledge */
/* Select people who have taken the pledge (and have an image) */
$pledgeCandidates = array_filter($candidate_data, function ($candidate) {
return $candidate['Pledge'] === 'y';
return $candidate['Pledge'] === 'Yes' && $candidate['Picture'] !== "";
});
/* Select 9 random candidates */
$pledgeKeys = array_rand($pledgeCandidates, 9);
$pledgeKeys = array_rand($pledgeCandidates, min(count($pledgeCandidates), 9));
shuffle($pledgeKeys);
$i = 0;

View File

@@ -1,7 +1,7 @@
<?php
class SPLPageRenderer {
public function renderPledgePage($councils, $candidates) {
public function renderPledgePage($councils, $lga_pages, $candidates) {
ob_start();
$didError = false;

View File

@@ -1,10 +1,15 @@
<?php
function parse_pledge_data($candidates_files) {
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);
@@ -13,18 +18,50 @@ function parse_pledge_data($candidates_files) {
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];
}
$candidate['Council'] = $config['councilName'];
$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";
@@ -40,7 +77,7 @@ function parse_pledge_data($candidates_files) {
$candidate['image_url'] = $media['url'];
$candidate['image_id'] = $media['id'];
}
$candidate_data[$candidate['Candidate Name']] = $candidate;
$candidate_data[$name_slug] = $candidate;
}
fclose($handle);
} else {
@@ -59,7 +96,7 @@ function parse_pledge_data($candidates_files) {
foreach ($headers as $key => $value) {
$candidate[$value] = $data[$key];
}
$candidate_data[$candidate['Candidate Name']]['Pledge'] =
$candidate_data[trim_sluggify($candidate['Candidate Name'])]['Pledge'] =
$candidate['Pledge'];
}
fclose($handle);

View File

@@ -3,7 +3,7 @@
require_once("parse_pledge_data.php");
require_once("page_renderer.php");
$options = getopt("", ["candidates-files:"]);
$options = getopt("", ["candidates-files:", "default-image:"]);
if (isset($options['candidates-files'])) {
$candidates_files = $options['candidates-files'];
@@ -12,26 +12,49 @@ if (isset($options['candidates-files'])) {
exit(1);
}
$candidate_data = parse_pledge_data(explode(" ", $candidates_files));
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';
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, $pledgeCandidates);
$pageContent = $renderer->renderPledgePage($councils, $lga_pages, $pledgeCandidates);
if ($pageContent === null) {
exit(2);

View File

@@ -5,15 +5,15 @@ function sluggify($input) {
?>
<!-- wp:paragraph -->
<p>The Streets People Love campaign offers council candidates the opportunity to take the following pledge:</p>
<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 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>
<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 councils have taken the pledge:</p>
<p>Candidates from these divisions have taken the pledge:</p>
<!-- /wp:paragraph -->
<?php
@@ -44,9 +44,9 @@ $councilCount = count($councils);
<!-- 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): ?>
<?php foreach($councilChunks[$columnIdx] as $divisionName): ?>
<!-- wp:list-item -->
<li><a href="#<?php echo sluggify($councilName); ?>"><?php echo $councilName; ?></a></li>
<li><a href="#<?php echo sluggify($divisionName); ?>"><?php echo $divisionName; ?></a></li>
<!-- /wp:list-item -->
<?php endforeach; ?>
@@ -69,7 +69,7 @@ $councilCount = count($councils);
<?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 $groupSlug; ?>"><?php echo htmlspecialchars($council); ?></a></h3>
<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
@@ -97,6 +97,12 @@ $councilCount = count($councils);
<?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'];
@@ -107,7 +113,11 @@ $councilCount = count($councils);
?>
<!-- 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"><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:heading {"textAlign":"center","className":"wp-block-heading has-text-align-center has-medium-font-size","style":{"spacing":{"margin":{"top":"1rem","bottom":"0.5rem"}}}} -->

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

View File

@@ -11,10 +11,14 @@
#wp post create --post_type=page --post_title="test_pledge" movie-homepage
#wp post update 1803 ../spl-data/movie-homepage
DATA_PATH="../spl-data"
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
@@ -24,4 +28,4 @@ pledge_sed=$(php pledge-update/homepage.php --candidates-files "${candidates_fil
content=$(sed "$pledge_sed" ../spl-data/movie-homepage)
wp post update 1803 --post_content="$content"
echo "$content" | wp post update 1803 -

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"