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.
 
 
 
 

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