Election results in the London Borough of Sutton.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

249 lignes
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 ]