Election results in the London Borough of Sutton.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

293 lines
7.1 KiB

  1. #!/usr/bin/env ruby
  2. # Generate a static site
  3. require 'logger'
  4. require 'haml'
  5. require_relative '../models'
  6. OUTPUT_DIR = '_site'
  7. VIEWS_DIR = File.join('..', 'views')
  8. LAYOUT_FN = File.join(VIEWS_DIR, 'layout.haml')
  9. @log = Logger.new($stdout)
  10. @log.level = Logger::INFO
  11. @log.info "Build starts."
  12. @log.info "Output directory is: #{OUTPUT_DIR}"
  13. class String
  14. def pluralize(num)
  15. if num == 1
  16. return self
  17. end
  18. case self[-1]
  19. when 'y'
  20. self[0..-2] + 'ies'
  21. when 's'
  22. self + "es"
  23. else
  24. self + "s"
  25. end
  26. end
  27. end
  28. def commify(num)
  29. num.to_s.reverse.gsub(/(\d\d\d)(?=\d)(?!\d*\.)/,'\1,').reverse
  30. end
  31. # From http://snippets.dzone.com/posts/show/593
  32. def to_ordinal(num)
  33. num = num.to_i
  34. if (10...20) === num
  35. "#{num}th"
  36. else
  37. g = %w{ th st nd rd th th th th th th }
  38. a = num.to_s
  39. c = a[-1..-1].to_i
  40. a + g[c]
  41. end
  42. end
  43. def format_percent(num)
  44. sprintf("%.0f%%", num)
  45. end
  46. def short_date(d)
  47. d.strftime("%e %b %Y")
  48. end
  49. def long_date(d)
  50. d.strftime("%e %B %Y")
  51. end
  52. # Exception for Labour/Co-operative candidacies
  53. def party_name(labcoop, party_name)
  54. labcoop ? "Labour and Co-operative Party" : party_name
  55. end
  56. def write_page(path_items, template, locals = {})
  57. dir = File.join(path_items)
  58. FileUtils.mkdir_p(dir)
  59. @log.debug dir
  60. fn = File.join(dir, 'index.html')
  61. # https://stackoverflow.com/questions/6125265/using-layouts-in-haml-files-independently-of-rails
  62. html = Haml::Engine.new(File.read(LAYOUT_FN)).render do
  63. Haml::Engine.new(File.read(File.join(VIEWS_DIR, "#{template}.haml"))).render(Object.new, locals)
  64. end
  65. File.write(fn, html)
  66. @log.info fn
  67. # TODO - add page to sitemap.xml or sitemap.txt
  68. # https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190
  69. end
  70. test_dir = File.join(Dir.pwd, OUTPUT_DIR)
  71. # FIXME - clear output directory at the start of every run
  72. # FileUtils.rm_rf(test_dir)
  73. Dir.mkdir(test_dir) unless File.directory?(test_dir)
  74. Dir.chdir(test_dir)
  75. # Copy `public` dir to output dir
  76. FileUtils.copy_entry(File.join('..', 'public'), '.')
  77. # Home page
  78. locals = {
  79. future_elections: Election.future,
  80. past_elections: Election.past
  81. }
  82. write_page('.', 'index', locals)
  83. # Election pages
  84. Election.each do |e|
  85. locals = {
  86. body: Body.first(:slug => e.body.slug),
  87. election: Election.first(:body => e.body, :d => e.d),
  88. elections_for_this_body: Election.all(:body => e.body, :order => [:d]),
  89. total_seats: Candidacy.sum(:seats, :election => e),
  90. total_votes: Candidacy.sum(:votes, :election => e)
  91. }
  92. # There's got to be a better way to do this, either with SQL or Datamapper
  93. locals['total_districts'] = repository(:default).adapter.select("
  94. SELECT district_id
  95. FROM candidacies
  96. WHERE election_id = ?
  97. GROUP BY district_id
  98. ORDER BY district_id
  99. ", e.id).count
  100. locals['results_by_party'] = repository(:default).adapter.select("
  101. SELECT
  102. p.colour,
  103. p.name,
  104. SUM(c.votes) AS votez,
  105. SUM(c.seats) AS seatz,
  106. COUNT(*) AS cands
  107. FROM candidacies c
  108. LEFT JOIN parties p ON p.id = c.party_id
  109. WHERE c.election_id = ?
  110. GROUP BY c.party_id, p.colour, p.name
  111. ORDER BY seatz DESC, votez DESC
  112. ", e.id)
  113. write_page(['bodies', e.body.slug, 'elections', e.d.to_s], 'electionsummary', locals)
  114. # District results for this election (resultsdistrict)
  115. # Loop through all districts in this election
  116. e.candidacies.districts.each do |d|
  117. total_seats = Candidacy.sum(:seats, :district => d, :election => e)
  118. total_votes = Candidacy.sum(:votes, :district => d, :election => e)
  119. poll = Poll.get(d.id, e.id)
  120. locals = {
  121. district: d,
  122. body: d.body,
  123. election: e,
  124. candidacies: Candidacy.all(:district => d, :election => e, :order => [:position]),
  125. total_votes: total_votes,
  126. total_candidates: Candidacy.count(:district => d, :election => e),
  127. total_seats: total_seats,
  128. districts_in_this_election: e.candidacies.districts,
  129. poll: poll
  130. }
  131. locals['share_message'] = nil
  132. if total_seats == 1
  133. locals['share_denominator'] = total_votes
  134. elsif poll && poll.valid_ballot_papers
  135. locals['share_denominator'] = poll.valid_ballot_papers
  136. else
  137. locals['share_denominator'] = total_votes / total_seats
  138. locals['share_message'] = "The vote share percentages have been estimated as we don't have data for the number of valid ballot papers in this poll."
  139. end
  140. # Postgres: All the columns selected when using GROUP BY must either be aggregate functions or appear in the GROUP BY clause
  141. locals['results_by_party'] = repository(:default).adapter.select("
  142. SELECT
  143. p.name AS party_name,
  144. p.colour AS party_colour,
  145. COUNT(c.id) AS num_candidates,
  146. SUM(c.seats) AS num_seats,
  147. SUM(c.votes) AS total_votes
  148. FROM candidacies c
  149. LEFT JOIN parties p
  150. ON c.party_id = p.id
  151. WHERE c.district_id = ?
  152. AND c.election_id = ?
  153. GROUP BY p.name, p.colour
  154. ORDER BY total_votes DESC
  155. ", d.id, e.id)
  156. write_page(['bodies', e.body.slug, 'elections', e.d.to_s, e.body.districts_name, d.slug], 'resultsdistrict', locals)
  157. end
  158. end
  159. # Candidate index
  160. locals = { candidates: Candidate.all(:order => [ :surname, :forenames ]) }
  161. write_page('candidates', 'candidates', locals)
  162. # Candidate pages
  163. # FIXME: What do we do about deleted candidates/redirects?
  164. Candidate.each do |c|
  165. locals = {
  166. candidate: c
  167. }
  168. locals['candidacies'] = repository(:default).adapter.select("
  169. SELECT
  170. e.d,
  171. c.*,
  172. p.name AS party_name,
  173. p.colour AS party_colour,
  174. b.name AS body_name,
  175. b.slug AS body_slug,
  176. b.districts_name AS districts_name,
  177. d.name AS district_name,
  178. d.slug AS district_slug
  179. FROM candidacies c
  180. INNER JOIN elections e
  181. ON c.election_id = e.id
  182. INNER JOIN parties p
  183. ON c.party_id = p.id
  184. INNER JOIN bodies b
  185. ON e.body_id = b.id
  186. INNER JOIN districts d
  187. ON c.district_id = d.id
  188. WHERE c.candidate_id = ?
  189. ORDER BY d
  190. ", c.id)
  191. write_page(['candidates', c.id.to_s], 'candidate', locals)
  192. end
  193. # Bodies index
  194. dir = 'bodies'
  195. FileUtils.mkdir_p(dir)
  196. @log.debug dir
  197. fn = File.join(dir, 'index.html')
  198. FileUtils.touch(fn) # empty file
  199. @log.info fn
  200. # Body detail pages
  201. Body.each do |b|
  202. locals = {
  203. body: b,
  204. districts: District.all(:body => b, :order => [:name])
  205. }
  206. locals['elections'] = repository(:default).adapter.select("
  207. SELECT
  208. e.id,
  209. e.kind,
  210. e.d,
  211. SUM(p.ballot_papers_issued)::float / SUM(p.electorate) * 100 AS turnout_percent
  212. FROM elections e
  213. LEFT JOIN polls p
  214. ON e.id = p.election_id
  215. WHERE e.body_id = ?
  216. GROUP BY p.election_id, e.id
  217. ORDER BY e.d DESC
  218. ", b.id)
  219. write_page(['bodies', b.slug], 'body', locals)
  220. # Districts for this body
  221. b.districts.each do |d|
  222. locals = {
  223. district: d,
  224. body: b
  225. }
  226. write_page(['bodies', b.slug, b.districts_name, d.slug], 'district', locals)
  227. end
  228. end
  229. write_page('about', 'about')
  230. write_page('guides', 'guides')
  231. write_page(%w(guides how-the-parliament-election-works), 'parliament')
  232. write_page(%w(guides how-the-council-election-works), 'election')
  233. @log.info "Build complete."