diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | index.html | 19 | ||||
-rwxr-xr-x | proposals.py | 126 | ||||
-rw-r--r-- | script.js | 30 | ||||
-rw-r--r-- | style.css | 16 |
6 files changed, 194 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..204a590 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +proposals.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31de770 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +all: + ./proposals.py > proposals.json diff --git a/index.html b/index.html new file mode 100644 index 0000000..5521bc3 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> + <title>OSM proposals</title> + <link rel=stylesheet href=style.css /> + <h1>OSM proposals</h1> + <table border=1> + <thead> + <th>Status</th> + <th>Name</th> + <th>Draft start</th> + <th>RFC start</th> + <th>Vote start</th> + <th>Authors</th> + </thead> + <tbody id=tbody> + </tbody> + </table> + <script src=script.js></script> +</html> diff --git a/proposals.py b/proposals.py new file mode 100755 index 0000000..66c5b1b --- /dev/null +++ b/proposals.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import html +import json +import sys + +import pywikiapi +import mwparserfromhell + +OSMWIKI_ENDPOINT = 'https://wiki.openstreetmap.org/w/api.php' + +osmwiki = pywikiapi.Site(OSMWIKI_ENDPOINT) + +# https://wiki.openstreetmap.org/w/index.php?title=Template:Proposal_page&action=edit + +CATEGORIES = ( + 'Category:Proposed features under way', + 'Category:Active features', + 'Category:Inactive features', +) + + +def find_proposals(): + for cat in CATEGORIES: + yield from osmwiki.query_pages( + generator='categorymembers', + gcmtitle=cat, + gcmlimit='max', + prop='revisions', + rvprop='content', + rvslots='main', + ) + + +def get_template_val(tpl, name): + param = tpl.get(name, None) + if param: + value = param.value.strip() + if value: + # turn empty strings into None + return value + + +proposals = [] + +for page in find_proposals(): + text = page['revisions'][0]['slots']['main']['content'] + doc = mwparserfromhell.parse(text) + templates = doc.filter_templates(matches=lambda t: t.name.matches('Proposal Page')) + + if not templates: + print('{{Proposal Page}} not found in ', page['title'], file=sys.stderr) + continue + + for comment in doc.ifilter_comments(): + # remove comments like <!-- Date the RFC email is sent to the Tagging list: YYYY-MM-DD --> + doc.remove(comment) + + tpl = templates[0] + page_title = page['title'] + + name = get_template_val(tpl, 'name') + if name: + name = html.unescape(name) + + status = get_template_val(tpl, 'status') + if status: + status = status.lower() + + draft_start = get_template_val(tpl, 'draftStartDate') + if draft_start in ('*', '-'): + draft_start = None + + rfc_start = get_template_val(tpl, 'rfcStartDate') + if rfc_start in ('*', '-'): + rfc_start = None + + vote_start = get_template_val(tpl, 'voteStartDate') + if vote_start in ('*', '-'): + vote_start = None + + definition = get_template_val(tpl, 'definition') + users = get_template_val(tpl, 'users') or get_template_val(tpl, 'user') + + proposals.append( + dict( + page_title=page_title, + name=name, + status=status, + definition=definition, + draft_start=draft_start, + rfc_start=rfc_start, + vote_start=vote_start, + authors=users, + ) + ) + +STATUSES = { + 'voting': 0, + 'proposed': 1, + 'draft': 2, + 'post-vote': 3, + 'approved': 4, + 'inactive': 5, + 'rejected': 6, + 'abandoned': 7, + 'canceled': 8, + 'obsoleted': 9, +} + + +def sort_key(proposal): + status = proposal['status'] + + if status in ('voting', 'approved', 'rejected'): + date = proposal['vote_start'] or '' + elif status == 'proposed': + date = proposal['rfc_start'] or '' + else: + date = proposal['draft_start'] or '' + + return (-STATUSES.get(proposal['status'], 10), date) + + +proposals.sort(key=sort_key, reverse=True) + +json.dump(proposals, sys.stdout) diff --git a/script.js b/script.js new file mode 100644 index 0000000..b202914 --- /dev/null +++ b/script.js @@ -0,0 +1,30 @@ +const tbody = document.getElementById('tbody'); + +function newEl(tagname, content) { + const cell = document.createElement(tagname); + if (content instanceof Node) + cell.appendChild(content); + else + cell.textContent = content; + return cell; +} + +(async function() { + const data = await (await fetch('proposals.json')).json(); + data.forEach(proposal => { + const row = document.createElement('tr'); + + const statusCell = newEl('td', proposal.status); + statusCell.className = 'status-' + proposal.status; + row.appendChild(statusCell); + const link = newEl('a', proposal.name); + link.href = 'https://wiki.openstreetmap.org/wiki/' + proposal.page_title.replaceAll(' ', '_'); + row.appendChild(newEl('td', link)); + row.appendChild(newEl('td', proposal.draft_start)); + row.appendChild(newEl('td', proposal.rfc_start)); + row.appendChild(newEl('td', proposal.vote_start)); + row.appendChild(newEl('td', proposal.authors)); + + tbody.appendChild(row); + }); +})(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..c2159e6 --- /dev/null +++ b/style.css @@ -0,0 +1,16 @@ +body { + font-family: sans-serif; +} +a { + text-decoration: none; +} +th { + position: sticky; + top: 0; + background: gainsboro; +} +td:first-of-type { text-align: center; } +.status-voting { background: #baf; } +.status-proposed { background: #eef; } +.status-approved { background: #dfd; } +.status-rejected { background: #fbb; } |