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.
 
 
 
 

249 lines
6.6 KiB

  1. #!/usr/bin/env ruby
  2. # Generate a static site
  3. # https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
  4. t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  5. require 'logger'
  6. require 'haml'
  7. require_relative '../models'
  8. require_relative '../lib/helpers'
  9. OUTPUT_DIR = '_site'
  10. VIEWS_DIR = File.join('..', 'views')
  11. LAYOUT_FN = File.join(VIEWS_DIR, 'layout.haml')
  12. @log = Logger.new($stdout)
  13. @log.level = Logger::INFO
  14. @log.info "Build starts."
  15. @log.info "Output directory is: #{OUTPUT_DIR}"
  16. @pages = 0
  17. def write_page(path_items, template, locals = {})
  18. dir = File.join(path_items)
  19. FileUtils.mkdir_p(dir)
  20. @log.debug dir
  21. fn = File.join(dir, 'index.html')
  22. # https://stackoverflow.com/questions/6125265/using-layouts-in-haml-files-independently-of-rails
  23. html = Haml::Engine.new(File.read(LAYOUT_FN)).render do
  24. Haml::Engine.new(File.read(File.join(VIEWS_DIR, "#{template}.haml"))).render(Object.new, locals)
  25. end
  26. File.write(fn, html)
  27. @log.info fn
  28. @pages += 1
  29. # TODO - add page to sitemap.xml or sitemap.txt
  30. # https://support.google.com/webmasters/answer/183668?hl=en&ref_topic=4581190
  31. end
  32. working_dir = File.join(Dir.pwd, OUTPUT_DIR)
  33. # Recursively delete working directory to ensure no redundant files are left behind from previous builds.
  34. FileUtils.rm_rf(working_dir)
  35. Dir.mkdir(working_dir) unless File.directory?(working_dir)
  36. Dir.chdir(working_dir)
  37. # Copy `public` dir to output dir
  38. FileUtils.copy_entry(File.join('..', 'public'), '.')
  39. # Home page
  40. locals = {
  41. future_elections: Election.future,
  42. past_elections: Election.past
  43. }
  44. write_page('.', 'index', locals)
  45. # Election pages
  46. Election.each do |e|
  47. locals = {
  48. body: Body.first(:slug => e.body.slug),
  49. election: Election.first(:body => e.body, :d => e.d),
  50. elections_for_this_body: Election.all(:body => e.body, :order => [:d]),
  51. total_seats: Candidacy.sum(:seats, :election => e),
  52. total_votes: Candidacy.sum(:votes, :election => e)
  53. }
  54. # There's got to be a better way to do this, either with SQL or Datamapper
  55. locals['total_districts'] = repository(:default).adapter.select("
  56. SELECT district_id
  57. FROM candidacies
  58. WHERE election_id = ?
  59. GROUP BY district_id
  60. ORDER BY district_id
  61. ", e.id).count
  62. locals['results_by_party'] = repository(:default).adapter.select("
  63. SELECT
  64. p.colour,
  65. p.name,
  66. SUM(c.votes) AS votez,
  67. SUM(c.seats) AS seatz,
  68. COUNT(*) AS cands
  69. FROM candidacies c
  70. LEFT JOIN parties p ON p.id = c.party_id
  71. WHERE c.election_id = ?
  72. GROUP BY c.party_id, p.colour, p.name
  73. ORDER BY seatz DESC, votez DESC
  74. ", e.id)
  75. write_page(['bodies', e.body.slug, 'elections', e.d.to_s], 'electionsummary', locals)
  76. # District results for this election (resultsdistrict)
  77. # Loop through all districts in this election
  78. e.candidacies.districts.each do |d|
  79. total_seats = Candidacy.sum(:seats, :district => d, :election => e)
  80. total_votes = Candidacy.sum(:votes, :district => d, :election => e)
  81. poll = Poll.get(d.id, e.id)
  82. locals = {
  83. district: d,
  84. body: d.body,
  85. election: e,
  86. candidacies: Candidacy.all(:district => d, :election => e, :order => [:position]),
  87. total_votes: total_votes,
  88. total_candidates: Candidacy.count(:district => d, :election => e),
  89. total_seats: total_seats,
  90. districts_in_this_election: e.candidacies.districts,
  91. poll: poll
  92. }
  93. locals['share_message'] = nil
  94. if total_seats == 1
  95. locals['share_denominator'] = total_votes
  96. elsif poll && poll.valid_ballot_papers
  97. locals['share_denominator'] = poll.valid_ballot_papers
  98. else
  99. locals['share_denominator'] = total_votes / total_seats
  100. 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."
  101. end
  102. # Postgres: All the columns selected when using GROUP BY must either be aggregate functions or appear in the GROUP BY clause
  103. locals['results_by_party'] = repository(:default).adapter.select("
  104. SELECT
  105. p.name AS party_name,
  106. p.colour AS party_colour,
  107. COUNT(c.id) AS num_candidates,
  108. SUM(c.seats) AS num_seats,
  109. SUM(c.votes) AS total_votes
  110. FROM candidacies c
  111. LEFT JOIN parties p
  112. ON c.party_id = p.id
  113. WHERE c.district_id = ?
  114. AND c.election_id = ?
  115. GROUP BY p.name, p.colour
  116. ORDER BY total_votes DESC
  117. ", d.id, e.id)
  118. write_page(['bodies', e.body.slug, 'elections', e.d.to_s, e.body.districts_name, d.slug], 'resultsdistrict', locals)
  119. end
  120. end
  121. # Candidate index
  122. locals = { candidates: Candidate.all(:order => [ :surname, :forenames ]) }
  123. write_page('candidates', 'candidates', locals)
  124. # Candidate pages
  125. # FIXME: What do we do about deleted candidates/redirects?
  126. Candidate.each do |c|
  127. locals = {
  128. candidate: c
  129. }
  130. locals['candidacies'] = repository(:default).adapter.select("
  131. SELECT
  132. e.d,
  133. c.*,
  134. p.name AS party_name,
  135. p.colour AS party_colour,
  136. b.name AS body_name,
  137. b.slug AS body_slug,
  138. b.districts_name AS districts_name,
  139. d.name AS district_name,
  140. d.slug AS district_slug
  141. FROM candidacies c
  142. INNER JOIN elections e
  143. ON c.election_id = e.id
  144. INNER JOIN parties p
  145. ON c.party_id = p.id
  146. INNER JOIN bodies b
  147. ON e.body_id = b.id
  148. INNER JOIN districts d
  149. ON c.district_id = d.id
  150. WHERE c.candidate_id = ?
  151. ORDER BY d
  152. ", c.id)
  153. write_page(['candidates', c.id.to_s], 'candidate', locals)
  154. end
  155. # Bodies index
  156. dir = 'bodies'
  157. FileUtils.mkdir_p(dir)
  158. @log.debug dir
  159. fn = File.join(dir, 'index.html')
  160. FileUtils.touch(fn) # empty file
  161. @log.info fn
  162. # Body detail pages
  163. Body.each do |b|
  164. locals = {
  165. body: b,
  166. districts: District.all(:body => b, :order => [:name])
  167. }
  168. locals['elections'] = repository(:default).adapter.select("
  169. SELECT
  170. e.id,
  171. e.kind,
  172. e.d,
  173. SUM(p.ballot_papers_issued)::float / SUM(p.electorate) * 100 AS turnout_percent
  174. FROM elections e
  175. LEFT JOIN polls p
  176. ON e.id = p.election_id
  177. WHERE e.body_id = ?
  178. GROUP BY p.election_id, e.id
  179. ORDER BY e.d DESC
  180. ", b.id)
  181. write_page(['bodies', b.slug], 'body', locals)
  182. # Districts for this body
  183. b.districts.each do |d|
  184. locals = {
  185. district: d,
  186. body: b
  187. }
  188. write_page(['bodies', b.slug, b.districts_name, d.slug], 'district', locals)
  189. end
  190. end
  191. write_page('about', 'about')
  192. write_page('guides', 'guides')
  193. write_page(%w(guides how-the-parliament-election-works), 'parliament')
  194. write_page(%w(guides how-the-council-election-works), 'election')
  195. @log.info "Build complete. %d pages generated in %0.2f seconds." % [ @pages, Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start ]