commit 9776c93ff736233cb328c9cad2642f8b97485f2e Author: Adrian Short Date: Sun Jun 6 22:00:16 2010 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8959d4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data/ +*sqlite3 + diff --git a/README b/README new file mode 100644 index 0000000..e69de29 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..cc0818e --- /dev/null +++ b/app.rb @@ -0,0 +1,111 @@ +require 'rubygems' +require 'sinatra' +require 'sinatra-helpers/haml/partials' +require 'haml' +require 'lib/models' + + +get '/' do + @directorates = Directorate.all + +# @results = repository(:default).adapter.query(" +# SELECT p.name, +# sum(c.votes_2010) AS votes, +# p.colour +# +# FROM parties p, +# councilcandidates c +# +# WHERE p.id = c.party_id +# +# GROUP BY p.name, p.colour +# +# ORDER BY votes desc +# ;") + +# select p.name, count(c.*) AS seats +# FROM parties p, councilcandidates c +# GROUP BY p.id + + haml :home +end + +get '/directorates/:id' do + @directorate = Directorate.get(params[:id]) + haml :directorate +end + +get '/suppliers/:id.csv' do + @supplier = Supplier.get(params[:id]) + + headers "Content-Disposition" => "attachment;filename=supplier#{@supplier.id}.csv", + "Content-Type" => "application/octet-stream" + + result = "Date,Trans No,Directorate,Service,Amount ex. VAT\n" + + for payment in @supplier.payments + result += "#{payment.d.strftime("%d %b %Y")},#{payment.trans_no},\"#{payment.directorate.name}\",#{payment.service.name},#{sprintf("%0.2f", payment.amount)}\n" + end + + result + +end + +get '/suppliers/:id' do + @supplier = Supplier.get(params[:id]) + haml :supplier +end + +get '/suppliers/?' do + @suppliers = Supplier.all( :order => ['name'] ) + haml :suppliers +end + +get '/services/:id.csv' do + @service = Service.get(params[:id]) + + headers "Content-Disposition" => "attachment;filename=service#{@service.id}.csv", + "Content-Type" => "application/octet-stream" + + result = "Date,Trans No,Directorate,Supplier,Amount ex. VAT\n" + + for payment in @service.payments + result += "#{payment.d.strftime("%d %b %Y")},#{payment.trans_no},\"#{payment.directorate.name}\",#{payment.supplier.name},#{sprintf("%0.2f", payment.amount)}\n" + end + + result + +end + +get '/services/:id' do + @service = Service.get(params[:id]) + haml :service +end + +get '/services/?' do + @services = Service.all( :order => ['name'] ) + haml :services +end + +get '/wards/:slug/postcode/:postcode/?' do + @ward = Ward.first(:slug => params[:slug]) + @postcode = params[:postcode] + haml :wards +end + +get '/wards/:slug/?' do + @ward = Ward.first(:slug => params[:slug]) + haml :wards +end + +get '/error' do + haml :error +end + +get '/about' do + haml :about +end + +not_found do + haml :not_found +end \ No newline at end of file diff --git a/import2009Q4.rb b/import2009Q4.rb new file mode 100644 index 0000000..ff98dad --- /dev/null +++ b/import2009Q4.rb @@ -0,0 +1,45 @@ +require 'lib/models' +require 'csv' + +count = 0 + +CSV::Reader.parse(File.open('data/2009Q4.csv', 'rb')) do |row| +# 2009Q4 Columns: +# 0: Directorate +# 1: Updated +# 2: TransNo +# 3: Service +# 4: Cost Centre +# 5: Supplier Name +# 6: Amount excl vat +# 7: Type + + count += 1 + + if (count > 4) # skip first four lines that don't contain data + + p row + + directorate = Directorate.first_or_create(:name => row[0].strip) + service = Service.first_or_create(:name => row[3].strip) + supplier = Supplier.first_or_create(:name => row[5].strip) + + payment = Payment.first_or_create( + 'trans_no' => row[2], + 'directorate' => directorate, + 'service' => service, + 'supplier' => supplier, + 'cost_centre' => row[4].strip, + 'amount' => row[6].strip.gsub(/,/, ''), + 'd' => row[1], + 'tyype' => row[7].strip + ) + + unless payment.save + puts "ERROR: Failed to save payment" + payment.errors.each do |e| + puts e + end + end + end +end diff --git a/import2010Q1.rb b/import2010Q1.rb new file mode 100644 index 0000000..af02ca1 --- /dev/null +++ b/import2010Q1.rb @@ -0,0 +1,39 @@ +require 'lib/models' +require 'csv' + +count = 0 + +# 2010Q1: 0-Directorate,1-Updated,2-Service,3-Supplier Name,4-Amount excl vat £,5-Type + +CSV::Reader.parse(File.open('data/2010Q1.csv', 'rb')) do |row| + + + count += 1 + + if (count > 4) # skip first four lines that don't contain data + + p row + + directorate = Directorate.first_or_create(:name => row[0].strip) + service = Service.first_or_create(:name => row[2].strip) + supplier = Supplier.first_or_create(:name => row[3].strip) + + dt = row[1].strip.split('/') + + payment = Payment.first_or_create( + 'directorate' => directorate, + 'service' => service, + 'supplier' => supplier, + 'amount' => row[4].strip.gsub(/,/, ''), + 'd' => Date.new(dt[2].to_i, dt[1].to_i, dt[0].to_i), + 'tyype' => row[5].strip + ) + + unless payment.save + puts "ERROR: Failed to save payment" + payment.errors.each do |e| + puts e + end + end + end +end diff --git a/lib/models.rb b/lib/models.rb new file mode 100644 index 0000000..37c6c4a --- /dev/null +++ b/lib/models.rb @@ -0,0 +1,58 @@ +require 'rubygems' +require 'dm-core' +require 'dm-validations' +require 'dm-timestamps' +require 'dm-aggregates' + +class Payment + include DataMapper::Resource + + property :id, Serial + property :trans_no, Integer, :required => false # "TransNo" in RBWM CSV files + property :directorate_id, Integer, :required => true + property :service_id, Integer, :required => true + property :supplier_id, Integer, :required => true + property :cost_centre, String, :required => false + property :amount, BigDecimal, :precision => 10, :scale => 2, :required => true # ex VAT + property :d, Date, :required => true # "Updated" in RBWM CSV files + property :tyype, String, :required => true # Capital or Revenue + + belongs_to :directorate + belongs_to :service + belongs_to :supplier +end + + +class Directorate + include DataMapper::Resource + + property :id, Serial + property :name, String, :length => 255, :required => true + + has n, :payments, :order => ['d'] +end + +class Service + include DataMapper::Resource + + property :id, Serial + property :name, String, :length => 255, :required => true + + has n, :payments, :order => ['d'] +end + +class Supplier + include DataMapper::Resource + + property :id, Serial + property :name, String, :length => 255, :required => true + + has n, :payments, :order => ['d'] + +# def self.slugify(name) +# name.gsub(/[^\w\s-]/, '').gsub(/\s+/, '-').downcase +# end +end + +DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/db.sqlite3") +DataMapper.auto_upgrade! diff --git a/public/breadcrumb/bc_bg.gif b/public/breadcrumb/bc_bg.gif new file mode 100755 index 0000000..9a2367a Binary files /dev/null and b/public/breadcrumb/bc_bg.gif differ diff --git a/public/breadcrumb/bc_separator.gif b/public/breadcrumb/bc_separator.gif new file mode 100755 index 0000000..904a9eb Binary files /dev/null and b/public/breadcrumb/bc_separator.gif differ diff --git a/public/breadcrumb/breadcrumb.css b/public/breadcrumb/breadcrumb.css new file mode 100755 index 0000000..80b2f8a --- /dev/null +++ b/public/breadcrumb/breadcrumb.css @@ -0,0 +1,42 @@ + +#breadcrumb { + font: 11px Arial, Helvetica, sans-serif; + background-image:url('/breadcrumb/bc_bg.gif'); + background-repeat:repeat-x; + height:30px; + line-height:30px; + color:#888; + border:solid 1px #cacaca; + width:100%; + overflow:hidden; + margin:0px; + padding:0px; +} + +#breadcrumb li { + list-style-type:none; + padding-left:10px; + display:inline-block; + float:left; +} + +#breadcrumb a { + display:inline-block; + background-image:url('/breadcrumb/bc_separator.gif'); + background-repeat:no-repeat; + background-position:right; + padding-right: 15px; + text-decoration: none; + color:#333333; + outline:none; +} + +.home { + border:none; + margin: 7px 0px; + background-image:url('/breadcrumb/bc_separator.gif'); +} + +#breadcrumb a:hover { + color:#35acc5; +} \ No newline at end of file diff --git a/public/breadcrumb/home.gif b/public/breadcrumb/home.gif new file mode 100755 index 0000000..c76d002 Binary files /dev/null and b/public/breadcrumb/home.gif differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/grid.css b/public/grid.css new file mode 100644 index 0000000..013c748 --- /dev/null +++ b/public/grid.css @@ -0,0 +1,338 @@ +/* + Variable Grid System. + Learn more ~ http://www.spry-soft.com/grids/ + Based on 960 Grid System - http://960.gs/ + + Licensed under GPL and MIT. +*/ + + +/* Containers +----------------------------------------------------------------------------------------------------*/ +.container_12 { + margin-left: auto; + margin-right: auto; + width: 960px; +} + +/* Grid >> Global +----------------------------------------------------------------------------------------------------*/ + +.grid_1, +.grid_2, +.grid_3, +.grid_4, +.grid_5, +.grid_6, +.grid_7, +.grid_8, +.grid_9, +.grid_10, +.grid_11, +.grid_12 { + display:inline; + float: left; + position: relative; + margin-left: 15px; + margin-right: 15px; +} + +/* Grid >> Children (Alpha ~ First, Omega ~ Last) +----------------------------------------------------------------------------------------------------*/ + +.alpha { + margin-left: 0; +} + +.omega { + margin-right: 0; +} + +/* Grid >> 12 Columns +----------------------------------------------------------------------------------------------------*/ + +.container_12 .grid_1 { + width:50px; +} + +.container_12 .grid_2 { + width:130px; +} + +.container_12 .grid_3 { + width:210px; +} + +.container_12 .grid_4 { + width:290px; +} + +.container_12 .grid_5 { + width:370px; +} + +.container_12 .grid_6 { + width:450px; +} + +.container_12 .grid_7 { + width:530px; +} + +.container_12 .grid_8 { + width:610px; +} + +.container_12 .grid_9 { + width:690px; +} + +.container_12 .grid_10 { + width:770px; +} + +.container_12 .grid_11 { + width:850px; +} + +.container_12 .grid_12 { + width:930px; +} + + + +/* Prefix Extra Space >> 12 Columns +----------------------------------------------------------------------------------------------------*/ + +.container_12 .prefix_1 { + padding-left:80px; +} + +.container_12 .prefix_2 { + padding-left:160px; +} + +.container_12 .prefix_3 { + padding-left:240px; +} + +.container_12 .prefix_4 { + padding-left:320px; +} + +.container_12 .prefix_5 { + padding-left:400px; +} + +.container_12 .prefix_6 { + padding-left:480px; +} + +.container_12 .prefix_7 { + padding-left:560px; +} + +.container_12 .prefix_8 { + padding-left:640px; +} + +.container_12 .prefix_9 { + padding-left:720px; +} + +.container_12 .prefix_10 { + padding-left:800px; +} + +.container_12 .prefix_11 { + padding-left:880px; +} + + + +/* Suffix Extra Space >> 12 Columns +----------------------------------------------------------------------------------------------------*/ + +.container_12 .suffix_1 { + padding-right:80px; +} + +.container_12 .suffix_2 { + padding-right:160px; +} + +.container_12 .suffix_3 { + padding-right:240px; +} + +.container_12 .suffix_4 { + padding-right:320px; +} + +.container_12 .suffix_5 { + padding-right:400px; +} + +.container_12 .suffix_6 { + padding-right:480px; +} + +.container_12 .suffix_7 { + padding-right:560px; +} + +.container_12 .suffix_8 { + padding-right:640px; +} + +.container_12 .suffix_9 { + padding-right:720px; +} + +.container_12 .suffix_10 { + padding-right:800px; +} + +.container_12 .suffix_11 { + padding-right:880px; +} + + + +/* Push Space >> 12 Columns +----------------------------------------------------------------------------------------------------*/ + +.container_12 .push_1 { + left:80px; +} + +.container_12 .push_2 { + left:160px; +} + +.container_12 .push_3 { + left:240px; +} + +.container_12 .push_4 { + left:320px; +} + +.container_12 .push_5 { + left:400px; +} + +.container_12 .push_6 { + left:480px; +} + +.container_12 .push_7 { + left:560px; +} + +.container_12 .push_8 { + left:640px; +} + +.container_12 .push_9 { + left:720px; +} + +.container_12 .push_10 { + left:800px; +} + +.container_12 .push_11 { + left:880px; +} + + + +/* Pull Space >> 12 Columns +----------------------------------------------------------------------------------------------------*/ + +.container_12 .pull_1 { + left:-80px; +} + +.container_12 .pull_2 { + left:-160px; +} + +.container_12 .pull_3 { + left:-240px; +} + +.container_12 .pull_4 { + left:-320px; +} + +.container_12 .pull_5 { + left:-400px; +} + +.container_12 .pull_6 { + left:-480px; +} + +.container_12 .pull_7 { + left:-560px; +} + +.container_12 .pull_8 { + left:-640px; +} + +.container_12 .pull_9 { + left:-720px; +} + +.container_12 .pull_10 { + left:-800px; +} + +.container_12 .pull_11 { + left:-880px; +} + + + + +/* Clear Floated Elements +----------------------------------------------------------------------------------------------------*/ + +/* http://sonspring.com/journal/clearing-floats */ + +.clear { + clear: both; + display: block; + overflow: hidden; + visibility: hidden; + width: 0; + height: 0; +} + +/* http://perishablepress.com/press/2008/02/05/lessons-learned-concerning-the-clearfix-css-hack */ + +.clearfix:after { + clear: both; + content: ' '; + display: block; + font-size: 0; + line-height: 0; + visibility: hidden; + width: 0; + height: 0; +} + +.clearfix { + display: inline-block; +} + +* html .clearfix { + height: 1%; +} + +.clearfix { + display: block; +} \ No newline at end of file diff --git a/public/od_80x15_blue.png b/public/od_80x15_blue.png new file mode 100644 index 0000000..6314d59 Binary files /dev/null and b/public/od_80x15_blue.png differ diff --git a/public/print.css b/public/print.css new file mode 100644 index 0000000..edfde0f --- /dev/null +++ b/public/print.css @@ -0,0 +1,9 @@ +.noprint, #footer, #breadcrumb +{ + display: none; +} + +body +{ + margin: 0 auto; +} \ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..945bf2e --- /dev/null +++ b/public/style.css @@ -0,0 +1,121 @@ +body +{ + background-color: #fff; + color: #555; + font-family: Helvetica, Arial, sans-serif; + font-size: 100%; + line-height: 1.5em; +} + +p +{ + font-size: 110%; +} + +input +{ + font-size: 130%; + background-color: #fff; +} + + + +#main +{ + margin: 30px 0; +} + +#footer +{ + font-size: 100%; + background-color: #fff; + text-align: left; + margin: 40px 0 40px 0; +} + +a +{ + background-color: #dce9b0; + padding: 1px 4px; + color: #111; + text-decoration: none; +} + +a:visited +{ + background-color: #eee; + padding: 1px 4px; + color: #111; + text-decoration: none; +} + +a:hover +{ + background-color: #4f4f4f; + color: #fff; +} + +h1 +{ + margin-top: 20px; + line-height: 1.4em; + font-weight: bold; + color: #86a11d; +} + +h2 +{ + margin-top: 20px; + line-height: 1.5em; + font-weight: bold; + color: #86a11d; +} + +form +{ + font-size: 150%; +} + + + +.highlight +{ + background-color: #fff7c0; + padding: 5px; +} + +strong +{ + color: #000; +} + +table +{ + border-collapse: collapse; +} + +td, th +{ + padding: 6px; +} + +th +{ + text-align: left; +} + +tr +{ + border-bottom: 1px solid #eee; +} + +.right +{ + text-align: right; +} + + +.noborder +{ + border: 0; +} diff --git a/views/about.haml b/views/about.haml new file mode 100644 index 0000000..85a17ad --- /dev/null +++ b/views/about.haml @@ -0,0 +1,55 @@ +.grid_9 + %h1= @page_title = "About this website" + + %blockquote + The swift and simple changes we are calling for today [to encourage councils to publish spending data] will unleash an army of armchair auditors and quite rightly make those charged with doling out the pennies stop and think twice about whether they are getting value for money. + + %p.right — Eric Pickles, Communities & Local Government Secretary, 5 June 2010 + + + + %p.vcard + This website was designed and written by + %a.fn.url{ :href => 'http://adrianshort.co.uk' } Adrian Short + \. + You can contact me by email at + %a.email{ :href => "mailto:adrian.short@gmail.com" } adrian.short@gmail.com + and + %a.url{ :href => "http://twitter.com/adrianshort" } follow me on Twitter + \. + + %p.highlight + This site is made with + %a{ :href => 'http://www.rbwm.gov.uk/web/finance_payments_to_suppliers.htm' } + the Royal Borough of Windsor and Maidenhead's spending data + and is entirely indepdendent of the council. The council did not commission or pay for this website. + + %p Every page on this website prints beautifully. + + %p + This site is written in + %a{ :href => "http://www.ruby-lang.org/en/" }Ruby + \ using the + %a{ :href => "http://www.sinatrarb.com/" }Sinatra framework. + + %p + The code for this website is + %a{ :href => "http://github.com/adrianshort/Sutton-Elections" }open source and managed on Github. + It is hosted by + %a{ :href => "http://heroku.com/" }Heroku. + + %p + The page templates use + %a{ :href => "http://haml-lang.com/" }Haml + and SprySoft's + %a{ :href => "http://www.spry-soft.com/grids/" }Variable Grid System + \. The database is + %a{ :href => "http://www.sqlite.org/" }SQLite + for development and + %a{ :href => "http://www.postgresql.org/" }PostgreSQL + for production, abstracted through + %a{ :href => "http://datamapper.org/" }DataMapper. + %p + Source control and deployment is done with + %a{ :href => "http://git-scm.com/" }Git. + diff --git a/views/directorate.haml b/views/directorate.haml new file mode 100644 index 0000000..09dd019 --- /dev/null +++ b/views/directorate.haml @@ -0,0 +1,30 @@ +.grid_12 + %h2= @page_title = @directorate.name + " Directorate" + + %ul#breadcrumb + %li.home + %a{ :href => '/'} Home + %li + %a{ :href => '/directorates' } Directorates + %li + = @directorate.name + + %table + %tr + %th Date + %th Service + %th Supplier + %th £ + + - for payment in @directorate.payments + %tr + %td= payment.d.strftime("%d %b %Y") + %td + %a{ :href => '/services/' + payment.service.id.to_s } + = payment.service.name + %td + %a{ :href => '/suppliers/' + payment.supplier.id.to_s } + = payment.supplier.name + %td.right= sprintf("%0d", payment.amount) + + \ No newline at end of file diff --git a/views/error.haml b/views/error.haml new file mode 100644 index 0000000..2413721 --- /dev/null +++ b/views/error.haml @@ -0,0 +1,5 @@ +.grid_12 + %h1 Invalid postcode + %p + %a{ :href => "/" }Please go back and try again + \ No newline at end of file diff --git a/views/home.haml b/views/home.haml new file mode 100644 index 0000000..09e392a --- /dev/null +++ b/views/home.haml @@ -0,0 +1,11 @@ +.grid_12 + + %ul#breadcrumb + %li.home + + %h2 Directorates + + - for directorate in @directorates + %p + %a{ :href=> "/directorates/#{directorate.id}" } + = directorate.name diff --git a/views/layout.haml b/views/layout.haml new file mode 100644 index 0000000..ea40638 --- /dev/null +++ b/views/layout.haml @@ -0,0 +1,26 @@ +!!! XML +!!! +%html + %head + %title= @page_title ? @page_title + " - Armchair Auditor" : "Armchair Auditor" + %link{ :rel => 'stylesheet', :type => 'text/css', :href => '/style.css' } + %link{ :rel => 'stylesheet', :type => 'text/css', :href => '/breadcrumb/breadcrumb.css' } + %link{ :rel => 'stylesheet', :type => 'text/css', :href => '/print.css', :media => 'print' } + %link{ :rel => 'stylesheet', :type => 'text/css', :href => '/grid.css' } + + %body + .container_12 + .grid_12 + %h1 Royal Borough of Windsor & Maidenhead Armchair Auditor + = yield + .clear + #footer + .grid_12 + %p + %a{ :href => '/' } Home + %p + %a{ :href => '/services' } Services + %p + %a{ :href => '/suppliers' } Suppliers + %p + %a{ :href => '/about' } About this website diff --git a/views/not_found.haml b/views/not_found.haml new file mode 100644 index 0000000..d13fdff --- /dev/null +++ b/views/not_found.haml @@ -0,0 +1,7 @@ +.grid_9 + %h1 Not Found + + %p Oops, we can't find that page. + + %p + %a{ :href => '/' }Go back to the home page \ No newline at end of file diff --git a/views/service.haml b/views/service.haml new file mode 100644 index 0000000..55e3f80 --- /dev/null +++ b/views/service.haml @@ -0,0 +1,33 @@ +.grid_12 + %h2= @page_title = @service.name + " (Service)" + + %ul#breadcrumb + %li.home + %a{ :href => '/'} Home + %li + %a{ :href => '/services' } Services + %li + = @service.name + %p + %a{ :href => "/services/#{@service.id}.csv" } + Download data as CSV + + %table + %tr + %th Date + %th Directorate + %th Supplier + %th £ + + - for payment in @service.payments + %tr + %td= payment.d.strftime("%d %b %Y") + %td + %a{ :href => '/directorates/' + payment.directorate.id.to_s } + = payment.directorate.name + %td + %a{ :href => '/suppliers/' + payment.supplier.id.to_s } + = payment.supplier.name + %td.right= sprintf("%0d", payment.amount) + + \ No newline at end of file diff --git a/views/services.haml b/views/services.haml new file mode 100644 index 0000000..4bb8744 --- /dev/null +++ b/views/services.haml @@ -0,0 +1,13 @@ +.grid_12 + %h2 Services + + %ul#breadcrumb + %li.home + %a{ :href => '/'} Home + %li + Services + + - for service in @services + %p + %a{ :href=> "/services/#{service.id}" } + = service.name diff --git a/views/supplier.haml b/views/supplier.haml new file mode 100644 index 0000000..d579ae7 --- /dev/null +++ b/views/supplier.haml @@ -0,0 +1,34 @@ +.grid_12 + %h2= @page_title = @supplier.name + " (Supplier)" + + %ul#breadcrumb + %li.home + %a{ :href => '/'} Home + %li + %a{ :href => '/suppliers' } Suppliers + %li + = @supplier.name + + %p.noprint + %a{ :href => "/suppliers/#{@supplier.id}.csv" } + Download data as CSV + + %table + %tr + %th Date + %th Directorate + %th Service + %th £ + + - for payment in @supplier.payments + %tr + %td= payment.d.strftime("%d %b %Y") + %td + %a{ :href => '/directorates/' + payment.directorate.id.to_s } + = payment.directorate.name + %td + %a{ :href => '/services/' + payment.service.id.to_s } + = payment.service.name + %td.right= sprintf("%0d", payment.amount) + + \ No newline at end of file diff --git a/views/suppliers.haml b/views/suppliers.haml new file mode 100644 index 0000000..8870655 --- /dev/null +++ b/views/suppliers.haml @@ -0,0 +1,13 @@ +.grid_12 + %h2 Suppliers + + %ul#breadcrumb + %li.home + %a{ :href => '/'} Home + %li + Suppliers + + - for supplier in @suppliers + %p + %a{ :href=> "/suppliers/#{supplier.id}" } + = supplier.name