diff options
-rw-r--r-- | app/assets/images/widget-base.png | bin | 0 -> 569 bytes | |||
-rw-r--r-- | app/assets/javascripts/general.js | 8 | ||||
-rw-r--r-- | app/assets/stylesheets/widget.scss | 109 | ||||
-rw-r--r-- | app/controllers/track_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/widgets_controller.rb | 63 | ||||
-rw-r--r-- | app/helpers/widget_helper.rb | 46 | ||||
-rw-r--r-- | app/models/info_request.rb | 1 | ||||
-rw-r--r-- | app/models/widget_vote.rb | 19 | ||||
-rw-r--r-- | app/views/request/_act.html.erb | 5 | ||||
-rw-r--r-- | app/views/widgets/new.html.erb | 15 | ||||
-rw-r--r-- | app/views/widgets/show.html.erb | 48 | ||||
-rw-r--r-- | config/application.rb | 1 | ||||
-rw-r--r-- | config/general.yml-example | 8 | ||||
-rw-r--r-- | config/routes.rb | 2 | ||||
-rw-r--r-- | db/migrate/20140824191444_create_widget_votes.rb | 11 | ||||
-rw-r--r-- | doc/CHANGES.md | 3 | ||||
-rw-r--r-- | lib/configuration.rb | 1 | ||||
-rw-r--r-- | spec/controllers/request_controller_spec.rb | 1 | ||||
-rw-r--r-- | spec/controllers/widgets_controller_spec.rb | 181 | ||||
-rw-r--r-- | spec/factories/widget_votes.rb | 7 | ||||
-rw-r--r-- | spec/helpers/widget_helper_spec.rb | 28 | ||||
-rw-r--r-- | spec/models/widget_vote_spec.rb | 53 |
22 files changed, 609 insertions, 3 deletions
diff --git a/app/assets/images/widget-base.png b/app/assets/images/widget-base.png Binary files differnew file mode 100644 index 000000000..872244543 --- /dev/null +++ b/app/assets/images/widget-base.png diff --git a/app/assets/javascripts/general.js b/app/assets/javascripts/general.js index 856f4c6d4..639a6917b 100644 --- a/app/assets/javascripts/general.js +++ b/app/assets/javascripts/general.js @@ -57,4 +57,12 @@ $(document).ready(function() { $('#everypage').hide(); } + // "Create widget" page + $("#widgetbox").select() + // Chrome workaround + $("widgetbox").mouseup(function() { + // Prevent further mouseup intervention + $this.unbind("mouseup"); + return false; + }); }) diff --git a/app/assets/stylesheets/widget.scss b/app/assets/stylesheets/widget.scss new file mode 100644 index 000000000..a67e1f3f0 --- /dev/null +++ b/app/assets/stylesheets/widget.scss @@ -0,0 +1,109 @@ +/* CSS Mini Reset */ + +html, body, div, form, fieldset, legend, label +{ + margin: 0; + padding: 0; +} + +table +{ + border-collapse: collapse; + border-spacing: 0; +} + +th, td +{ + text-align: left; + vertical-align: top; +} + +h1, h2, h3, h4, h5, h6, th, td, caption { font-weight:normal; } + +img { border: 0; } + +body { + background-color: #ffffff; + color: #333333; + padding: 0; + margin: 0; + font-family: "Helvetica Neue", Arial, Helvetica, Helmet, Freesans, sans-serif; + font-weight: normal; + font-style: normal; + line-height: 1.5em; + position: relative; + cursor: default; + font-size: 1em; +} + +a:hover, +a:focus, +a:active { + color: #333; +} + +.alaveteli-widget { + width: 318px; + height: 213px; + border: 1px solid #e9e9e9; + background: #e9e9e9 url("widget-base.png") top left no-repeat; + position: relative; + border: 1px solid #eee; +} + +.alaveteli-widget__title { + position: absolute; + top: 1em; + left: 16px; + width: 160px; + height: 75px; + overflow: hidden; +} + +.alaveteli-widget__status { + position: absolute; + top: 97px; + left: 16px; + font-weight: bold; + text-transform: uppercase; +} + +.alaveteli-widget__status__status-label { + margin: 0; + font-weight: normal; + font-size: 0.875em; + line-height: 1.1em; + color: #555; + text-transform: capitalize; +} + +.alaveteli-widget__left { + position: absolute; + width: 145px; +} + +.alaveteli-widget__people-count { + position: absolute; + left: 192px; + top: 44px; + width: 100px; + text-align: center; + line-height: 1.3em; +} +.alaveteli-widget__count { + font-size: 55px; + line-height: 55px; + text-align: center; +} + +.alaveteli-widget__bottom a { + text-decoration: none; +} + +.alaveteli-widget__button { + position: absolute; + top: 173px; + left: 16px; + text-align: center; +} + diff --git a/app/controllers/track_controller.rb b/app/controllers/track_controller.rb index 4b272797f..8b8055b55 100644 --- a/app/controllers/track_controller.rb +++ b/app/controllers/track_controller.rb @@ -214,6 +214,4 @@ class TrackController < ApplicationController redirect_to URI.parse(params[:r]).path end - end - diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb new file mode 100644 index 000000000..0cc1008a1 --- /dev/null +++ b/app/controllers/widgets_controller.rb @@ -0,0 +1,63 @@ +# app/controllers/widget_controller.rb: +# Handle widgets, if enabled +# +# Copyright (c) 2014 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +require 'securerandom' + +class WidgetsController < ApplicationController + + before_filter :check_widget_config, :find_info_request, :check_prominence + skip_before_filter :set_x_frame_options_header, :only => [:show] + + def show + medium_cache + @track_thing = TrackThing.create_track_for_request(@info_request) + @status = @info_request.calculate_status + @count = @info_request.track_things.count + @info_request.widget_votes.count + 1 + + if @user + @existing_track = TrackThing.find_existing(@user, @track_thing) + end + unless @user || cookies[:widget_vote] + cookies.permanent[:widget_vote] = SecureRandom.hex(10) + end + render :action => 'show', :layout => false + end + + def new + long_cache + end + + # Track interest in a request from a non-logged in user + def update + if !@user && cookies[:widget_vote] + @info_request.widget_votes. + where(:cookie => cookies[:widget_vote]). + first_or_create + end + + track_thing = TrackThing.create_track_for_request(@info_request) + redirect_to do_track_path(track_thing), status => :temporary_redirect + end + + private + + def find_info_request + @info_request = InfoRequest.find(params[:request_id]) + end + + def check_widget_config + unless AlaveteliConfiguration::enable_widgets + raise ActiveRecord::RecordNotFound.new("Page not enabled") + end + end + + def check_prominence + unless @info_request.prominence == 'normal' + render :nothing => true, :status => :forbidden + end + end + +end diff --git a/app/helpers/widget_helper.rb b/app/helpers/widget_helper.rb new file mode 100644 index 000000000..e604954fe --- /dev/null +++ b/app/helpers/widget_helper.rb @@ -0,0 +1,46 @@ +module WidgetHelper + def status_description(info_request, status) + case status + when 'waiting_classification' + _('Awaiting classification') + when 'waiting_response' + _('Awaiting response') + when 'waiting_response_overdue' + _('Delayed') + when 'waiting_response_very_overdue' + _('Long overdue') + when 'not_held' + _('Not held') + when 'rejected' + _('Rejected') + when 'successful' + _('Successful') + when 'partially_successful' + _('Partial success') + when 'waiting_clarification' + _('Awaiting clarification') + when 'gone_postal' + _('Handled by post') + when 'internal_review' + _('Internal review') + when 'error_message' + _('Delivery error') + when 'requires_admin' + _('Unusual response') + when 'user_withdrawn' + _('Withdrawn') + when 'attention_requested' + _('Needs admin attention') + when 'vexatious' + _('Vexatious') + when 'not_foi' + _('Not an FOI request') + else + if info_request.respond_to?(:theme_display_status) + info_request.theme_display_status(status) + else + _('Unknown') + end + end + end +end
\ No newline at end of file diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 57a8fd7e0..f9f6cffa9 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -50,6 +50,7 @@ class InfoRequest < ActiveRecord::Base has_many :info_request_events, :order => 'created_at' has_many :user_info_request_sent_alerts has_many :track_things, :order => 'created_at desc' + has_many :widget_votes has_many :comments, :order => 'created_at' has_many :censor_rules, :order => 'created_at desc' has_many :mail_server_logs, :order => 'mail_server_log_done_id' diff --git a/app/models/widget_vote.rb b/app/models/widget_vote.rb new file mode 100644 index 000000000..021c38b30 --- /dev/null +++ b/app/models/widget_vote.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: widget_votes +# +# id :integer not null, primary key +# cookie :string(255) +# info_request_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class WidgetVote < ActiveRecord::Base + belongs_to :info_request + validates :info_request, :presence => true + + attr_accessible :cookie + validates :cookie, :length => { :is => 20 } + validates_uniqueness_of :cookie, :scope => :info_request_id +end diff --git a/app/views/request/_act.html.erb b/app/views/request/_act.html.erb index 878cdf4ff..c7bbd287f 100644 --- a/app/views/request/_act.html.erb +++ b/app/views/request/_act.html.erb @@ -20,3 +20,8 @@ <% end %> <%= link_to _("Start your own blog"), "http://wordpress.com/"%> </div> +<% if AlaveteliConfiguration::enable_widgets %> + <div class="act_link"> + <%= link_to _("Create a widget for this request"), new_request_widget_path(@info_request) %> + </div> +<% end %> diff --git a/app/views/widgets/new.html.erb b/app/views/widgets/new.html.erb new file mode 100644 index 000000000..c706155a5 --- /dev/null +++ b/app/views/widgets/new.html.erb @@ -0,0 +1,15 @@ +<h1>Add a widget</h1> + +<p> +To add a widget for <b><%= @info_request.title %></b>, copy and paste the +following code to your web page: +<textarea autofocus readonly rows='4' cols='60' id='widgetbox'> +<iframe src='<%= request_widget_url(@info_request) %>' width='320' height='215' frameborder='0' marginwidth='0' marginheight='0'></iframe> +</textarea> +</p> + +<p> +The widget will look like this: +<br> +<iframe src='<%= request_widget_url(@info_request) %>' width='320' height='215' frameborder='0' marginwidth='0' marginheight='0'></iframe> +</p> diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb new file mode 100644 index 000000000..07c7b1908 --- /dev/null +++ b/app/views/widgets/show.html.erb @@ -0,0 +1,48 @@ +<head> + <%= stylesheet_link_tag "widget" %> +</head> +<body> + <div class="alaveteli-widget"> + <div class="alaveteli-widget__top"> + <div class="alaveteli-widget__left"> + <div class="alaveteli-widget__title"> + <%= link_to @info_request.title, request_path(@info_request), :target => "_top" %> + </div> + <div class="alaveteli-widget__status <%= @status %>"> + <p class="alaveteli-widget__status__status-label">Status</p> + <%= status_description(@info_request, @status) %> + </div> + </div> + <div class="alaveteli-widget__people-count"> + <%= n_('<div class="alaveteli-widget__count">{{count}}</div> person wants to know', '<div class="alaveteli-widget__count">{{count}}</div> people want to know', @count, :count => @count) %> + </div> + </div> + + + <div class="alaveteli-widget__bottom"> + <% if @info_request.user && @info_request.user == @user %> + <div class="alaveteli-widget__button alaveteli-widget__button--down"> + <%= _('This is your request') %> + </div> + <% elsif @existing_track %> + <a href="<%= url_for :controller => 'track', :action => 'update', :track_id => @existing_track.id, :track_medium => "delete", :r => request.fullpath %>"> + <div class="alaveteli-widget__button--down"> + <%= _('You are tracking this request') %> + </div> + </a> + <% else %> + <% if @user %> + <a href="<%= url_for do_track_path(@track_thing) %>" target="_blank"> + <div class="alaveteli-widget__button"> + <%= _('I also want to know!') %> + </div> + </a> + <% else %> + <%= form_tag request_widget_url(@info_request), :method => 'put', :target => '_blank' do %> + <%= submit_tag _('I also want to know!'), :class => 'alaveteli-widget__button' %> + <% end %> + <% end %> + <% end %> + </div> + </div> +</body> diff --git a/config/application.rb b/config/application.rb index 3c01e26c4..472077f06 100644 --- a/config/application.rb +++ b/config/application.rb @@ -116,6 +116,7 @@ module Alaveteli 'ie6.css', 'ie7.css', 'bootstrap-dropdown.js', + 'widget.css', 'responsive/print.css', 'responsive/application-lte-ie7.css', 'responsive/application-ie8.css'] diff --git a/config/general.yml-example b/config/general.yml-example index 88d89958d..df140136c 100644 --- a/config/general.yml-example +++ b/config/general.yml-example @@ -802,3 +802,11 @@ RESPONSIVE_STYLING: true # --- PRODUCTION_MAILER_DELIVERY_METHOD: sendmail +# If ENABLE_WIDGETS is set to true, Alaveteli will allow the embedding of a +# 'widget' linking to a request on other sites. This widget will record +# how many people click on an 'I also want to know' button. +# +# ENABLE_WIDGETS - Boolean (default: false) +# +# --- +ENABLE_WIDGETS: false diff --git a/config/routes.rb b/config/routes.rb index c975d6007..7319f92fc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,7 @@ Alaveteli::Application.routes.draw do resources :request, :only => [] do resource :report, :only => [:new, :create] + resource :widget, :only => [:new, :show, :update] end resources :info_request_batch, :only => :show @@ -142,6 +143,7 @@ Alaveteli::Application.routes.draw do match '/track/update/:track_id' => 'track#update', :as => :update match '/track/delete_all_type' => 'track#delete_all_type', :as => :delete_all_type match '/track/feed/:track_id' => 'track#atom_feed', :as => :atom_feed + match '/track/widget_vote/:info_request_id' => 'track#widget_vote', :as => :widget_vote #### #### Help controller diff --git a/db/migrate/20140824191444_create_widget_votes.rb b/db/migrate/20140824191444_create_widget_votes.rb new file mode 100644 index 000000000..0a7b4856a --- /dev/null +++ b/db/migrate/20140824191444_create_widget_votes.rb @@ -0,0 +1,11 @@ +class CreateWidgetVotes < ActiveRecord::Migration + def change + create_table :widget_votes do |t| + t.string :cookie + t.belongs_to :info_request, :null => false + + t.timestamps + end + add_index :widget_votes, :info_request_id + end +end diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 9b54a0b52..b9406fac3 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -4,6 +4,9 @@ * There is experimental support for using an STMP server, rather than sendmail, for outgoing mail. There is not yet any ability to retry if the SMTP server is unavailable. +* HTML 'widgets' advertising requests can be displayed on other sites in iframes. + If 'ENABLE_WIDGETS' is set to true in `general.yml` (the default is false), a link + to the widget code will appear in the right hand sidebar of a request page. # Version 0.21 diff --git a/lib/configuration.rb b/lib/configuration.rb index 7921b93fa..c983152e0 100644 --- a/lib/configuration.rb +++ b/lib/configuration.rb @@ -32,6 +32,7 @@ module AlaveteliConfiguration :DISABLE_EMERGENCY_USER => false, :DOMAIN => 'localhost:3000', :DONATION_URL => '', + :ENABLE_WIDGETS => false, :EXCEPTION_NOTIFICATIONS_FROM => '', :EXCEPTION_NOTIFICATIONS_TO => '', :FORCE_REGISTRATION_ON_NEW_REQUEST => false, diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 02237b29d..c4f3c847e 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2764,4 +2764,3 @@ describe RequestController, "#select_authorities" do end end - diff --git a/spec/controllers/widgets_controller_spec.rb b/spec/controllers/widgets_controller_spec.rb new file mode 100644 index 000000000..6a58c7c5c --- /dev/null +++ b/spec/controllers/widgets_controller_spec.rb @@ -0,0 +1,181 @@ +# coding: utf-8 +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe WidgetsController do + + include LinkToHelper + + describe "#show" do + + before do + @info_request = FactoryGirl.create(:info_request) + AlaveteliConfiguration.stub!(:enable_widgets).and_return(true) + end + + it 'should render the widget template' do + get :show, :request_id => @info_request.id + expect(response).to render_template('show') + end + + it 'should find the info request' do + get :show, :request_id => @info_request.id + assigns[:info_request].should == @info_request + end + + it 'should create a track thing for the request' do + get :show, :request_id => @info_request.id + assigns[:track_thing].info_request.should == @info_request + end + + it 'should assign the request status' do + get :show, :request_id => @info_request.id + assigns[:status].should == @info_request.calculate_status + end + + it 'should not send an x-frame-options header' do + get :show, :request_id => @info_request.id + response.headers["X-Frame-Options"].should be_nil + end + + context 'for a non-logged-in user' do + + context 'if no widget-vote cookie is set' do + + it 'should set a widget-vote cookie' do + cookies[:widget_vote].should be_nil + get :show, :request_id => @info_request.id + cookies[:widget_vote].should_not be_nil + end + + end + + end + + context 'when widgets are not enabled' do + + it 'should return a 404' do + AlaveteliConfiguration.stub!(:enable_widgets).and_return(false) + lambda{ get :show, :request_id => @info_request.id }.should + raise_error(ActiveRecord::RecordNotFound) + end + + end + + context "when the request's prominence is not 'normal'" do + + it 'should return a 403' do + @info_request.prominence = 'hidden' + @info_request.save! + get :show, :request_id => @info_request.id + response.code.should == "403" + end + + end + + end + + describe "#new" do + + before do + @info_request = FactoryGirl.create(:info_request) + AlaveteliConfiguration.stub!(:enable_widgets).and_return(true) + end + + it 'should render the create widget template' do + get :new, :request_id => @info_request.id + expect(response).to render_template('new') + end + + it 'should find the info request' do + get :new, :request_id => @info_request.id + assigns[:info_request].should == @info_request + end + + context 'when widgets are not enabled' do + + it 'should return a 404' do + AlaveteliConfiguration.stub!(:enable_widgets).and_return(false) + lambda{ get :new, :request_id => @info_request.id }.should + raise_error(ActiveRecord::RecordNotFound) + end + + end + + context "when the request's prominence is not 'normal'" do + + it 'should return a 403' do + @info_request.prominence = 'hidden' + @info_request.save! + get :show, :request_id => @info_request.id + response.code.should == "403" + end + + end + + end + + describe :update do + + before do + @info_request = FactoryGirl.create(:info_request) + AlaveteliConfiguration.stub!(:enable_widgets).and_return(true) + end + + it 'should find the info request' do + get :update, :request_id => @info_request.id + assigns[:info_request].should == @info_request + end + + it 'should redirect to the track path for the info request' do + get :update, :request_id => @info_request.id + track_thing = TrackThing.create_track_for_request(@info_request) + expect(response).to redirect_to(do_track_path(track_thing)) + end + + context 'when there is no logged-in user and a widget vote cookie' do + + before do + @cookie_value = 'x' * 20 + end + + it 'should create a widget vote if none exists for the info request and cookie' do + @info_request.widget_votes.where(:cookie => @cookie_value).size.should == 0 + request.cookies['widget_vote'] = @cookie_value + get :update, :request_id => @info_request.id + @info_request.widget_votes.where(:cookie => @cookie_value).size.should == 1 + end + + it 'should not create a widget vote if one exists for the info request and cookie' do + @info_request.widget_votes.create(:cookie => @cookie_value) + request.cookies['widget_vote'] = @cookie_value + get :update, :request_id => @info_request.id + @info_request.widget_votes.where(:cookie => @cookie_value).size.should == 1 + end + + end + + context 'when widgets are not enabled' do + + it 'should raise ActiveRecord::RecordNotFound' do + AlaveteliConfiguration.stub!(:enable_widgets).and_return(false) + lambda{ get :update, :request_id => @info_request.id }.should + raise_error(ActiveRecord::RecordNotFound) + end + + end + + context "when the request's prominence is not 'normal'" do + + it 'should return a 403' do + @info_request.prominence = 'hidden' + @info_request.save! + get :show, :request_id => @info_request.id + response.code.should == "403" + end + + end + + end + +end + diff --git a/spec/factories/widget_votes.rb b/spec/factories/widget_votes.rb new file mode 100644 index 000000000..964bbb20d --- /dev/null +++ b/spec/factories/widget_votes.rb @@ -0,0 +1,7 @@ +require 'securerandom' +FactoryGirl.define do + factory :widget_vote do + info_request + cookie { SecureRandom.hex(10) } + end +end
\ No newline at end of file diff --git a/spec/helpers/widget_helper_spec.rb b/spec/helpers/widget_helper_spec.rb new file mode 100644 index 000000000..c8c41b14f --- /dev/null +++ b/spec/helpers/widget_helper_spec.rb @@ -0,0 +1,28 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe WidgetHelper do + + include WidgetHelper + + describe :status_description do + + before do + @info_request = FactoryGirl.build(:info_request) + end + + it 'should return "Awaiting classification" for "waiting_classification' do + expect(status_description(@info_request, 'waiting_classification')).to eq('Awaiting classification') + end + + it 'should call theme_display_status for a theme status' do + @info_request.stub!(:theme_display_status).and_return("Special status") + expect(status_description(@info_request, 'special_status')).to eq('Special status') + end + + it 'should return unknown for an unknown status' do + expect(status_description(@info_request, 'special_status')).to eq('Unknown') + end + + end + +end
\ No newline at end of file diff --git a/spec/models/widget_vote_spec.rb b/spec/models/widget_vote_spec.rb new file mode 100644 index 000000000..b9f990eac --- /dev/null +++ b/spec/models/widget_vote_spec.rb @@ -0,0 +1,53 @@ +# == Schema Information +# +# Table name: widget_votes +# +# id :integer not null, primary key +# cookie :string(255) +# info_request_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# + +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe WidgetVote do + + describe :new do + + it 'requires an info request' do + widget_vote = WidgetVote.new + widget_vote.should_not be_valid + widget_vote.errors[:info_request].should == ["can't be blank"] + end + + it 'validates the cookie length' do + widget_vote = WidgetVote.new + widget_vote.should_not be_valid + widget_vote.errors[:cookie].should == ["is the wrong length (should be 20 characters)"] + end + + it 'is valid with a cookie and info request' do + widget_vote = FactoryGirl.create(:widget_vote) + widget_vote.should be_valid + end + + it 'enforces uniqueness of cookie per info request' do + info_request = FactoryGirl.create(:info_request) + widget_vote = info_request.widget_votes.create(:cookie => 'x' * 20) + duplicate_vote = info_request.widget_votes.build(:cookie => 'x' * 20) + duplicate_vote.should_not be_valid + duplicate_vote.errors[:cookie].should == ["has already been taken"] + end + + it 'allows the same cookie to be used across info requests' do + info_request = FactoryGirl.create(:info_request) + second_info_request = FactoryGirl.create(:info_request) + widget_vote = info_request.widget_votes.create(:cookie => 'x' * 20) + second_request_vote = second_info_request.widget_votes.build(:cookie => 'x' * 20) + second_request_vote.should be_valid + end + + end + +end |