summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile2
-rw-r--r--index.html19
-rwxr-xr-xproposals.py126
-rw-r--r--script.js30
-rw-r--r--style.css16
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; }