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.
 
 
 
 

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