diff options
Diffstat (limited to 'app')
50 files changed, 511 insertions, 321 deletions
diff --git a/app/assets/stylesheets/responsive/_footer_layout.scss b/app/assets/stylesheets/responsive/_footer_layout.scss index 2b0c956fa..55b6839c2 100644 --- a/app/assets/stylesheets/responsive/_footer_layout.scss +++ b/app/assets/stylesheets/responsive/_footer_layout.scss @@ -45,6 +45,10 @@ img { display: inherit; + @include lte-ie7 { + display: block; + } } + } } diff --git a/app/assets/stylesheets/responsive/_global_style.scss b/app/assets/stylesheets/responsive/_global_style.scss index 5b268f3a6..290591b5f 100644 --- a/app/assets/stylesheets/responsive/_global_style.scss +++ b/app/assets/stylesheets/responsive/_global_style.scss @@ -222,4 +222,7 @@ div.pagination { } - +/* Search result highlighting */ +.highlight { + background:#FF0; +} diff --git a/app/controllers/admin_request_controller.rb b/app/controllers/admin_request_controller.rb index fc291d998..21120e4ad 100644 --- a/app/controllers/admin_request_controller.rb +++ b/app/controllers/admin_request_controller.rb @@ -100,7 +100,8 @@ class AdminRequestController < AdminController @info_request.fully_destroy # expire cached files expire_for_request(@info_request) - flash[:notice] = "Request #{url_title} has been completely destroyed. Email of user who made request: " + user.email + email = user.try(:email) ? user.email : 'This request is external so has no associated user' + flash[:notice] = "Request #{ url_title } has been completely destroyed. Email of user who made request: #{ email }" redirect_to admin_request_list_url end @@ -199,7 +200,7 @@ class AdminRequestController < AdminController end # Bejeeps, look, sometimes a URL is something that belongs in a controller, jesus. - # XXX hammer this square peg into the round MVC hole + # TODO: hammer this square peg into the round MVC hole post_redirect = PostRedirect.new( :uri => upload_response_url(:url_title => info_request.url_title), :user_id => user.id) @@ -253,7 +254,7 @@ class AdminRequestController < AdminController end info_request_event.described_state = 'waiting_clarification' info_request_event.calculated_state = 'waiting_clarification' - # XXX deliberately don't update described_at so doesn't reenter search? + # TODO: deliberately don't update described_at so doesn't reenter search? info_request_event.save! flash[:notice] = "Old response marked as having been a clarification" diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index e6b0c121a..6f83d89d6 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -1,5 +1,9 @@ class ApiController < ApplicationController before_filter :check_api_key + before_filter :check_external_request, + :only => [:add_correspondence, :update_state] + before_filter :check_request_ownership, + :only => [:add_correspondence, :update_state] def show_request @request = InfoRequest.find(params[:id]) @@ -9,16 +13,11 @@ class ApiController < ApplicationController :id => @request.id, :url => make_url("request", @request.url_title), :title => @request.title, - :created_at => @request.created_at, :updated_at => @request.updated_at, - :status => @request.calculate_status, - :public_body_url => make_url("body", @request.public_body.url_name), - :request_email => @request.incoming_email, - :request_text => @request.last_event_forming_initial_request.outgoing_message.body, } if @request.user @@ -73,35 +72,19 @@ class ApiController < ApplicationController 'url' => make_url("request", request.url_title), 'id' => request.id } - end def add_correspondence - request = InfoRequest.find_by_id(params[:id]) - if request.nil? - render :json => { "errors" => ["Could not find request #{params[:id]}"] }, :status => 404 - return - end - json = ActiveSupport::JSON.decode(params[:correspondence_json]) attachments = params[:attachments] direction = json["direction"] body = json["body"] sent_at = json["sent_at"] + new_state = params["state"] errors = [] - if !request.is_external? - render :json => { "errors" => ["Request #{params[:id]} cannot be updated using the API"] }, :status => 500 - return - end - - if request.public_body_id != @public_body.id - render :json => { "errors" => ["You do not own request #{params[:id]}"] }, :status => 500 - return - end - if !["request", "response"].include?(direction) errors << "The direction parameter must be 'request' or 'response'" end @@ -116,6 +99,10 @@ class ApiController < ApplicationController errors << "You cannot attach files to messages in the 'request' direction" end + if new_state && !InfoRequest.allowed_incoming_states.include?(new_state) + errors << "'#{new_state}' is not a valid request state" + end + if !errors.empty? render :json => { "errors" => errors }, :status => 500 return @@ -125,16 +112,16 @@ class ApiController < ApplicationController # In the 'request' direction, i.e. what we (Alaveteli) regard as outgoing outgoing_message = OutgoingMessage.new( - :info_request => request, + :info_request => @request, :status => 'ready', :message_type => 'followup', :body => body, :last_sent_at => sent_at, :what_doing => 'normal_sort' ) - request.outgoing_messages << outgoing_message - request.save! - request.log_event("followup_sent", + @request.outgoing_messages << outgoing_message + @request.save! + @request.log_event("followup_sent", :api => true, :email => nil, :outgoing_message_id => outgoing_message.id, @@ -154,12 +141,48 @@ class ApiController < ApplicationController ) end - mail = RequestMailer.external_response(request, body, sent_at, attachment_hashes) + mail = RequestMailer.external_response(@request, body, sent_at, attachment_hashes) + + @request.receive(mail, mail.encoded, true) - request.receive(mail, mail.encoded, true) + if new_state + # we've already checked above that the status is valid + # so no need to check a second time + event = @request.log_event("status_update", + { :script => "#{@public_body.name} via API", + :old_described_state => @request.described_state, + :described_state => new_state, + }) + @request.set_described_state(new_state) + end end render :json => { - 'url' => make_url("request", request.url_title), + 'url' => make_url("request", @request.url_title), + } + end + + def update_state + new_state = params["state"] + + if InfoRequest.allowed_incoming_states.include?(new_state) + ActiveRecord::Base.transaction do + event = @request.log_event("status_update", + { :script => "#{@public_body.name} on behalf of requester via API", + :old_described_state => @request.described_state, + :described_state => new_state, + }) + @request.set_described_state(new_state) + end + else + render :json => { + "errors" => ["'#{new_state}' is not a valid request state" ] + }, + :status => 500 + return + end + + render :json => { + 'url' => make_url("request", @request.url_title), } end @@ -168,51 +191,48 @@ class ApiController < ApplicationController raise PermissionDenied.new("#{@public_body.id} != #{params[:id]}") if @public_body.id != params[:id].to_i since_date_str = params[:since_date] - if since_date_str.nil? - @events = InfoRequestEvent.find_by_sql([ - %(select info_request_events.* - from info_requests - join info_request_events on info_requests.id = info_request_events.info_request_id - where info_requests.public_body_id = ? - and info_request_events.event_type in ( - 'sent', 'followup_sent', 'resent', 'followup_resent' - ) - order by info_request_events.created_at desc - ), @public_body.id - ]) - else + since_event_id = params[:since_event_id] + + event_type_clause = "event_type in ('sent', 'followup_sent', 'resent', 'followup_resent')" + + @events = InfoRequestEvent.where(event_type_clause) \ + .joins(:info_request) \ + .where("public_body_id = ?", @public_body.id) \ + .includes([{:info_request => :user}, :outgoing_message]) \ + .order('info_request_events.created_at DESC') + + if since_date_str begin - since_date = Date.strptime(since_date_str, "%Y-%m-%d") + since_date = Date.strptime(since_date_str, "%Y-%m-%d") rescue ArgumentError - render :json => {"errors" => [ - "Parameter since_date must be in format yyyy-mm-dd (not '#{since_date_str}')" ] }, - :status => 500 - return + render :json => {"errors" => [ + "Parameter since_date must be in format yyyy-mm-dd (not '#{since_date_str}')" ] }, + :status => 500 + return end - @events = InfoRequestEvent.find_by_sql([ - %(select info_request_events.* - from info_requests - join info_request_events on info_requests.id = info_request_events.info_request_id - where info_requests.public_body_id = ? - and info_request_events.event_type in ( - 'sent', 'followup_sent', 'resent', 'followup_resent' - ) - and info_request_events.created_at >= ? - order by info_request_events.created_at desc - ), @public_body.id, since_date - ]) + @events = @events.where("info_request_events.created_at >= ?", since_date) + end + + # We take a "since" parameter that allows the client + # to restrict to events more recent than a certain other event + if since_event_id + begin + event = InfoRequestEvent.find(since_event_id) + rescue ActiveRecord::RecordNotFound + render :json => {"errors" => [ + "Event ID #{since_event_id} not found" ] }, + :status => 500 + return + end + @events = @events.where("info_request_events.created_at > ?", event.created_at) end + + if feed_type == "atom" render :template => "api/request_events", :formats => ['atom'], :layout => false elsif feed_type == "json" - # For the JSON feed, we take a "since" parameter that allows the client - # to restrict to events more recent than a certain other event - if params[:since_event_id] - @since_event_id = params[:since_event_id].to_i - end @event_data = [] @events.each do |event| - break if event.id == @since_event_id request = event.info_request this_event = { @@ -224,7 +244,6 @@ class ApiController < ApplicationController :request_email => request.incoming_email, :title => request.title, :body => event.outgoing_message.body, - :user_name => request.user_name, } if request.user @@ -246,6 +265,21 @@ class ApiController < ApplicationController raise PermissionDenied if @public_body.nil? end + def check_external_request + @request = InfoRequest.find_by_id(params[:id]) + if @request.nil? + render :json => { "errors" => ["Could not find request #{params[:id]}"] }, :status => 404 + elsif !@request.is_external? + render :json => { "errors" => ["Request #{params[:id]} cannot be updated using the API"] }, :status => 403 + end + end + + def check_request_ownership + if @request.public_body_id != @public_body.id + render :json => { "errors" => ["You do not own request #{params[:id]}"] }, :status => 403 + end + end + private def make_url(*args) "http://" + AlaveteliConfiguration::domain + "/" + args.join("/") diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 78a82316a..0c5f5bd02 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -278,10 +278,10 @@ class ApplicationController < ActionController::Base session[:post_redirect_token] = post_redirect.token - # XXX what is the built in Ruby URI munging function that can do this + # TODO: what is the built in Ruby URI munging function that can do this # choice of & vs. ? more elegantly than this dumb if statement? if uri.include?("?") - # XXX This looks odd. What would a fragment identifier be doing server-side? + # TODO: This looks odd. What would a fragment identifier be doing server-side? # But it also looks harmless, so I’ll leave it just in case. if uri.include?("#") uri.sub!("#", "&post_redirect=1#") diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb index 5e39c3a2c..2c0037577 100644 --- a/app/controllers/comment_controller.rb +++ b/app/controllers/comment_controller.rb @@ -21,7 +21,7 @@ class CommentController < ApplicationController end if params[:comment] - # XXX this check should theoretically be a validation rule in the model + # TODO: this check should theoretically be a validation rule in the model @existing_comment = Comment.find_existing(@info_request.id, params[:comment][:body]) else # Default to subscribing to request when first viewing form diff --git a/app/controllers/general_controller.rb b/app/controllers/general_controller.rb index 6f0d29889..158492eb2 100644 --- a/app/controllers/general_controller.rb +++ b/app/controllers/general_controller.rb @@ -59,7 +59,7 @@ class GeneralController < ApplicationController # Actual search def search - # XXX Why is this so complicated with arrays and stuff? Look at the route + # TODO: Why is this so complicated with arrays and stuff? Look at the route # in config/routes.rb for comments. combined = params[:combined].split("/") @sortby = nil @@ -70,7 +70,7 @@ class GeneralController < ApplicationController else @advanced = false end - # XXX currently /described isn't linked to anywhere, just used in RSS and for /list/successful + # TODO: currently /described isn't linked to anywhere, just used in RSS and for /list/successful # This is because it's confusingly different from /newest - but still useful for power users. if combined.size > 0 && (['newest', 'described', 'relevant'].include?(combined[-1])) @sort_postfix = combined.pop @@ -124,7 +124,7 @@ class GeneralController < ApplicationController end end - # Query each type separately for separate display (XXX we are calling + # Query each type separately for separate display (TODO: we are calling # perform_search multiple times and it clobbers per_page for each one, # so set as separate var) requests_per_page = params[:requests_per_page] ? params[:requests_per_page].to_i : 25 @@ -159,7 +159,7 @@ class GeneralController < ApplicationController end # Spelling and highight words are same for all three queries - @highlight_words = @request_for_spelling.words_to_highlight + @highlight_words = @request_for_spelling.words_to_highlight(:regex => true, :include_original => true) if !(@request_for_spelling.spelling_correction =~ /[a-z]+:/) @spelling_correction = @request_for_spelling.spelling_correction end @@ -178,7 +178,9 @@ class GeneralController < ApplicationController format.json { render :json => { :alaveteli_git_commit => alaveteli_git_commit, :alaveteli_version => ALAVETELI_VERSION, - :ruby_version => RUBY_VERSION + :ruby_version => RUBY_VERSION, + :visible_request_count => InfoRequest.visible.count, + :confirmed_user_count => User.where(:email_confirmed => true).count }} end end diff --git a/app/controllers/public_body_change_requests_controller.rb b/app/controllers/public_body_change_requests_controller.rb index 4a6c5f5cb..773308546 100644 --- a/app/controllers/public_body_change_requests_controller.rb +++ b/app/controllers/public_body_change_requests_controller.rb @@ -1,5 +1,7 @@ class PublicBodyChangeRequestsController < ApplicationController + before_filter :catch_spam, :only => [:create] + def create @change_request = PublicBodyChangeRequest.from_params(params[:public_body_change_request], @user) if @change_request.save @@ -23,6 +25,16 @@ class PublicBodyChangeRequestsController < ApplicationController else @title = _('Ask us to add an authority') end + end + + private + def catch_spam + if params[:public_body_change_request].key?(:comment) + unless params[:public_body_change_request][:comment].empty? + redirect_to frontpage_url + end + end end + end diff --git a/app/controllers/public_body_controller.rb b/app/controllers/public_body_controller.rb index 862f4b318..d2c84d820 100644 --- a/app/controllers/public_body_controller.rb +++ b/app/controllers/public_body_controller.rb @@ -5,12 +5,11 @@ # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ -require 'fastercsv' require 'confidence_intervals' require 'tempfile' class PublicBodyController < ApplicationController - # XXX tidy this up with better error messages, and a more standard infrastructure for the redirect to canonical URL + # TODO: tidy this up with better error messages, and a more standard infrastructure for the redirect to canonical URL def show long_cache if MySociety::Format.simplify_url_part(params[:url_name], 'body') != params[:url_name] @@ -43,7 +42,7 @@ class PublicBodyController < ApplicationController query = InfoRequestEvent.make_query_from_params(params.merge(:latest_status => @view)) query += " requested_from:#{@public_body.url_name}" # Use search query for this so can collapse and paginate easily - # XXX really should just use SQL query here rather than Xapian. + # TODO: really should just use SQL query here rather than Xapian. sortby = "described" begin @xapian_requests = perform_search([InfoRequestEvent], query, sortby, 'request_collapse') @@ -86,7 +85,7 @@ class PublicBodyController < ApplicationController def list long_cache - # XXX move some of these tag SQL queries into has_tag_string.rb + # TODO: move some of these tag SQL queries into has_tag_string.rb like_query = params[:public_body_query] like_query = "" if like_query.nil? @@ -109,17 +108,17 @@ class PublicBodyController < ApplicationController # Restrict the public bodies shown according to the tag # parameter supplied in the URL: - if @tag.nil? or @tag == "all" - @tag = "all" + if @tag.nil? || @tag == 'all' + @tag = 'all' elsif @tag == 'other' - category_list = PublicBodyCategories::get().tags().map{|c| "'"+c+"'"}.join(",") + category_list = PublicBodyCategories.get.tags.map{ |c| %Q('#{ c }') }.join(",") where_condition += base_tag_condition + " AND has_tag_string_tags.name in (#{category_list})) = 0" elsif @tag.scan(/./mu).size == 1 - @tag = Unicode.upcase @tag + @tag = Unicode.upcase(@tag) # The first letter queries have to be done on # translations, so just indicate to add that later: first_letter = true - elsif @tag.include?(":") + elsif @tag.include?(':') name, value = HasTagString::HasTagStringTag.split_tag_into_name_value(@tag) where_condition += base_tag_condition + " AND has_tag_string_tags.name = ? AND has_tag_string_tags.value = ?) > 0" where_parameters.concat [name, value] @@ -128,16 +127,16 @@ class PublicBodyController < ApplicationController where_parameters.concat [@tag] end - if @tag == "all" - @description = "" + if @tag == 'all' + @description = '' elsif @tag.size == 1 - @description = _("beginning with ‘{{first_letter}}’", :first_letter=>@tag) + @description = _("beginning with ‘{{first_letter}}’", :first_letter => @tag) else - category_name = PublicBodyCategories::get().by_tag()[@tag] + category_name = PublicBodyCategories.get.by_tag[@tag] if category_name.nil? - @description = _("matching the tag ‘{{tag_name}}’", :tag_name=>@tag) + @description = _("matching the tag ‘{{tag_name}}’", :tag_name => @tag) else - @description = _("in the category ‘{{category_name}}’", :category_name=>category_name) + @description = _("in the category ‘{{category_name}}’", :category_name => category_name) end end @@ -151,15 +150,15 @@ class PublicBodyController < ApplicationController FROM public_bodies LEFT OUTER JOIN public_body_translations as current_locale ON (public_bodies.id = current_locale.public_body_id - AND current_locale.locale = ? AND #{get_public_body_list_translated_condition 'current_locale', first_letter}) + AND current_locale.locale = ? AND #{ get_public_body_list_translated_condition('current_locale', first_letter) }) LEFT OUTER JOIN public_body_translations as default_locale ON (public_bodies.id = default_locale.public_body_id - AND default_locale.locale = ? AND #{get_public_body_list_translated_condition 'default_locale', first_letter}) - WHERE #{where_condition} AND COALESCE(current_locale.name, default_locale.name) IS NOT NULL + AND default_locale.locale = ? AND #{ get_public_body_list_translated_condition('default_locale', first_letter) }) + WHERE #{ where_condition } AND COALESCE(current_locale.name, default_locale.name) IS NOT NULL ORDER BY display_name} - sql = [query, underscore_locale, like_query, like_query] + sql = [query, underscore_locale, like_query, like_query, like_query] sql.push @tag if first_letter - sql += [underscore_default_locale, like_query, like_query] + sql += [underscore_default_locale, like_query, like_query, like_query] sql.push @tag if first_letter sql += where_parameters @public_bodies = PublicBody.paginate_by_sql( @@ -170,17 +169,17 @@ class PublicBodyController < ApplicationController # The simpler case where we're just searching in the current locale: where_condition = get_public_body_list_translated_condition('public_body_translations', first_letter, true) + ' AND ' + where_condition - where_sql = [where_condition, like_query, like_query] + where_sql = [where_condition, like_query, like_query, like_query] where_sql.push @tag if first_letter where_sql += [underscore_locale] + where_parameters - @public_bodies = PublicBody.where(where_sql) \ - .joins(:translations) \ - .order("public_body_translations.name") \ - .paginate(:page => params[:page], :per_page => 100) + @public_bodies = PublicBody.where(where_sql). + joins(:translations). + order("public_body_translations.name"). + paginate(:page => params[:page], :per_page => 100) end respond_to do |format| - format.html { render :template => "public_body/list" } + format.html { render :template => 'public_body/list' } end end end @@ -191,6 +190,9 @@ class PublicBodyController < ApplicationController redirect_to list_public_bodies_url(:tag => @tag) end + # GET /body/all-authorities.csv + # + # Returns all public bodies (except for the internal admin authority) as CSV def list_all_csv # FIXME: this is just using the download directory for zip # archives, since we know that is allowed for X-Sendfile and @@ -198,21 +200,29 @@ class PublicBodyController < ApplicationController # used for the zips. However, really there should be a # generically named downloads directory that contains all # kinds of downloadable assets. - download_directory = File.join(InfoRequest.download_zip_dir(), - 'download') - FileUtils.mkdir_p download_directory + download_directory = File.join(InfoRequest.download_zip_dir, 'download') + FileUtils.mkdir_p(download_directory) output_leafname = 'all-authorities.csv' - output_filename = File.join download_directory, output_leafname + output_filename = File.join(download_directory, output_leafname) # Create a temporary file in the same directory, so we can # rename it atomically to the intended filename: - tmp = Tempfile.new output_leafname, download_directory + tmp = Tempfile.new(output_leafname, download_directory) tmp.close - # Export all the public bodies to that temporary path and make - # it readable: - PublicBody.export_csv tmp.path - FileUtils.chmod 0644, tmp.path - # Rename into place and send the file: - File.rename tmp.path, output_filename + + # Create the CSV + csv = PublicBodyCSV.new + PublicBody.visible.find_each(:include => [:translations, :tags]) do |public_body| + next if public_body.site_administration? + csv << public_body + end + + # Export all the public bodies to that temporary path, make it readable, + # and rename it + File.open(tmp.path, 'w') { |file| file.write(csv.generate) } + FileUtils.chmod(0644, tmp.path) + File.rename(tmp.path, output_filename) + + # Send the file send_file(output_filename, :type => 'text/csv; charset=utf-8; header=present', :filename => 'all-authorities.csv', @@ -344,9 +354,11 @@ class PublicBodyController < ApplicationController end private + def get_public_body_list_translated_condition(table, first_letter=false, locale=nil) result = "(upper(#{table}.name) LIKE upper(?)" \ - " OR upper(#{table}.notes) LIKE upper (?))" + " OR upper(#{table}.notes) LIKE upper(?)" \ + " OR upper(#{table}.short_name) LIKE upper(?))" if first_letter result += " AND #{table}.first_letter = ?" end diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index d66c28275..3fa0ef0ce 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -181,7 +181,7 @@ class RequestController < ApplicationController end @filters = params.merge(:latest_status => @view) - @title = _("View and search requests") + @title = _('Browse and search requests') @title = @title + " (page " + @page.to_s + ")" if (@page > 1) @track_thing = TrackThing.create_track_for_search_query(InfoRequestEvent.make_query_from_params(@filters)) @@ -303,8 +303,14 @@ class RequestController < ApplicationController return render_new_compose(batch=false) end + # Check we have :public_body_id - spammers seem to be using :public_body + # erroneously instead + if params[:info_request][:public_body_id].blank? + redirect_to frontpage_path and return + end + # See if the exact same request has already been submitted - # XXX this check should theoretically be a validation rule in the + # TODO: this check should theoretically be a validation rule in the # model, except we really want to pass @existing_request to the view so # it can link to it. @existing_request = InfoRequest.find_existing(params[:info_request][:title], params[:info_request][:public_body_id], params[:outgoing_message][:body]) @@ -359,7 +365,7 @@ class RequestController < ApplicationController end # This automatically saves dependent objects, such as @outgoing_message, in the same transaction @info_request.save! - # XXX send_message needs the database id, so we send after saving, which isn't ideal if the request broke here. + # TODO: send_message needs the database id, so we send after saving, which isn't ideal if the request broke here. @outgoing_message.send_message flash[:notice] = _("<p>Your {{law_used_full}} request has been <strong>sent on its way</strong>!</p> <p><strong>We will email you</strong> when there is a response, or after {{late_number_of_days}} working days if the authority still hasn't @@ -537,7 +543,7 @@ class RequestController < ApplicationController elsif @info_request_event.is_outgoing_message? redirect_to outgoing_message_url(@info_request_event.outgoing_message), :status => :moved_permanently else - # XXX maybe there are better URLs for some events than this + # TODO: maybe there are better URLs for some events than this redirect_to request_url(@info_request_event.info_request), :status => :moved_permanently end end @@ -1006,7 +1012,7 @@ class RequestController < ApplicationController params[:info_request][:public_body] = PublicBody.find(params[:url_name]) else public_body = PublicBody.find_by_url_name_with_historic(params[:url_name]) - raise ActiveRecord::RecordNotFound.new("None found") if public_body.nil? # XXX proper 404 + raise ActiveRecord::RecordNotFound.new("None found") if public_body.nil? # TODO: proper 404 params[:info_request][:public_body] = public_body end elsif params[:public_body_id] diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb index 97c47c448..dc4f783a6 100644 --- a/app/controllers/services_controller.rb +++ b/app/controllers/services_controller.rb @@ -31,7 +31,7 @@ class ServicesController < ApplicationController FastGettext.locale = old_fgt_locale end end - render :text => text, :content_type => "text/plain" # XXX workaround the HTML validation in test suite + render :text => text, :content_type => "text/plain" # TODO: workaround the HTML validation in test suite end def hidden_user_explanation diff --git a/app/controllers/track_controller.rb b/app/controllers/track_controller.rb index dccc52efc..83700a55b 100644 --- a/app/controllers/track_controller.rb +++ b/app/controllers/track_controller.rb @@ -82,7 +82,7 @@ class TrackController < ApplicationController def track_search_query @query = params[:query_array] - # XXX more hackery to make alternate formats still work with query_array + # TODO: more hackery to make alternate formats still work with query_array if /^(.*)\.json$/.match(@query) @query = $1 params[:format] = "json" @@ -154,7 +154,15 @@ class TrackController < ApplicationController request.format = 'xml' unless params[:format] respond_to do |format| format.json { render :json => @xapian_object.results.map { |r| r[:model].json_for_api(true, - lambda { |t| view_context.highlight_and_excerpt(t, @xapian_object.words_to_highlight, 150) } + lambda do |t| + view_context.highlight_and_excerpt( + t, + @xapian_object.words_to_highlight( + :regex => true, + :include_original => true), + 150 + ) + end ) } } format.any { render :template => 'track/atom_feed', :formats => ['atom'], diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 8d6522923..fcc500e06 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -46,7 +46,7 @@ class UserController < ApplicationController @is_you = !@user.nil? && @user.id == @display_user.id # Use search query for this so can collapse and paginate easily - # XXX really should just use SQL query here rather than Xapian. + # TODO: really should just use SQL query here rather than Xapian. if @show_requests begin requests_query = 'requested_by:' + @display_user.url_name @@ -102,11 +102,11 @@ class UserController < ApplicationController @is_you = !@user.nil? && @user.id == @display_user.id feed_results = Set.new # Use search query for this so can collapse and paginate easily - # XXX really should just use SQL query here rather than Xapian. + # TODO: really should just use SQL query here rather than Xapian. begin requests_query = 'requested_by:' + @display_user.url_name comments_query = 'commented_by:' + @display_user.url_name - # XXX combine these as OR query + # TODO: combine these as OR query @xapian_requests = perform_search([InfoRequestEvent], requests_query, 'newest', 'request_collapse') @xapian_comments = perform_search([InfoRequestEvent], comments_query, 'newest', nil) rescue @@ -121,7 +121,7 @@ class UserController < ApplicationController if @is_you @track_things = TrackThing.find(:all, :conditions => ["tracking_user_id = ? and track_medium = ?", @display_user.id, 'email_daily'], :order => 'created_at desc') for track_thing in @track_things - # XXX factor out of track_mailer.rb + # TODO: factor out of track_mailer.rb xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], track_thing.track_query, :sort_by_prefix => 'described_at', :sort_by_ascending => true, @@ -262,7 +262,7 @@ class UserController < ApplicationController end end - # Change password (XXX and perhaps later email) - requires email authentication + # Change password (TODO: and perhaps later email) - requires email authentication def signchangepassword if @user and ((not session[:user_circumstance]) or (session[:user_circumstance] != "change_password")) # Not logged in via email, so send confirmation @@ -288,7 +288,7 @@ class UserController < ApplicationController :reason_params => { :web => "", :email => _("Then you can change your password on {{site_name}}",:site_name=>site_name), - :email_subject => _("Change your password {{site_name}}",:site_name=>site_name) + :email_subject => _("Change your password on {{site_name}}",:site_name=>site_name) }, :circumstance => "change_password" # special login that lets you change your password ) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 33525cb3d..49ce94951 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -13,12 +13,18 @@ module ApplicationHelper # all of all. include LinkToHelper + # Some extra date and time formatters + include DateTimeHelper + # Site-wide access to configuration settings include ConfigHelper # Useful for sending emails include MailerHelper + # Extra highlight helpers + include HighlightHelper + # Copied from error_messages_for in active_record_helper.rb def foi_error_messages_for(*params) options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {} @@ -51,25 +57,6 @@ module ApplicationHelper end end - # Highlight words, also escapes HTML (other than spans that we add) - def highlight_words(t, words, html = true) - if html - highlight(h(t), words, :highlighter => '<span class="highlight">\1</span>').html_safe - else - highlight(t, words, :highlighter => '*\1*') - end - end - - def highlight_and_excerpt(t, words, excount, html = true) - newt = excerpt(t, words[0], :radius => excount) - if not newt - newt = excerpt(t, '', :radius => excount) - end - t = newt - t = highlight_words(t, words, html) - return t - end - def locale_name(locale) return LanguageNames::get_language_name(locale) end diff --git a/app/helpers/date_time_helper.rb b/app/helpers/date_time_helper.rb new file mode 100644 index 000000000..5f129e590 --- /dev/null +++ b/app/helpers/date_time_helper.rb @@ -0,0 +1,69 @@ +module DateTimeHelper + # Public: Usually-correct format for a DateTime-ish object + # To define a new new format define the `simple_date_{FORMAT}` method + # + # date - a DateTime, Date or Time + # opts - a Hash of options (default: { format: :html}) + # :format - :html returns a HTML <time> tag + # :text returns a plain String + # + # Examples + # + # simple_date(Time.now) + # # => "<time>..." + # + # simple_date(Time.now, :format => :text) + # # => "March 10, 2014" + # + # Returns a String + # Raises ArgumentError if the format is unrecognized + def simple_date(date, opts = {}) + opts = { :format => :html }.merge(opts) + date_formatter = "simple_date_#{ opts[:format] }" + + if respond_to?(date_formatter) + send(date_formatter, date) + else + raise ArgumentError, "Unrecognized format :#{ opts[:format] }" + end + end + + # Usually-correct HTML formatting of a DateTime-ish object + # Use LinkToHelper#simple_date with desired formatting options + # + # date - a DateTime, Date or Time + # + # Returns a String + def simple_date_html(date) + date = date.in_time_zone unless date.is_a?(Date) + time_tag date, simple_date_text(date), :title => date.to_s + end + + # Usually-correct plain text formatting of a DateTime-ish object + # Use LinkToHelper#simple_date with desired formatting options + # + # date - a DateTime, Date or Time + # + # Returns a String + def simple_date_text(date) + date = date.in_time_zone.to_date unless date.is_a? Date + + date_format = _('simple_date_format') + date_format = :long if date_format == 'simple_date_format' + I18n.l(date, :format => date_format) + end + + # Strips the date from a DateTime + # + # date - a DateTime, Date or Time + # + # Examples + # + # simple_time(Time.now) + # # => "10:46:54" + # + # Returns a String + def simple_time(date) + date.strftime("%H:%M:%S").strip + end +end diff --git a/app/helpers/highlight_helper.rb b/app/helpers/highlight_helper.rb new file mode 100644 index 000000000..a98f6f320 --- /dev/null +++ b/app/helpers/highlight_helper.rb @@ -0,0 +1,98 @@ +module HighlightHelper + include ERB::Util + + # Implementation of rails' highlight that allows regex to be passed to + # the phrases parameter. + # https://github.com/rails/rails/pull/11793 + def highlight_matches(text, phrases, options = {}) + text = ActionController::Base.helpers.sanitize(text).try(:html_safe) if options.fetch(:sanitize, true) + + if text.blank? || phrases.blank? + text + else + match = Array(phrases).map do |p| + Regexp === p ? p.to_s : Regexp.escape(p) + end.join('|') + + if block_given? + text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found } + else + highlighter = options.fetch(:highlighter, '<mark>\1</mark>') + text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + end + end.html_safe + end + + # Highlight words, also escapes HTML (other than spans that we add) + def highlight_words(t, words, html = true) + if html + highlight_matches(h(t), words, :highlighter => '<span class="highlight">\1</span>').html_safe + else + highlight_matches(t, words, :highlighter => '*\1*') + end + end + + def highlight_and_excerpt(t, words, excount, html = true) + newt = excerpt(t, words[0], :radius => excount) + if not newt + newt = excerpt(t, '', :radius => excount) + end + t = newt + t = highlight_words(t, words, html) + return t + end + + def excerpt(text, phrase, options = {}) + return unless text && phrase + + separator = options.fetch(:separator, nil) || "" + case phrase + when Regexp + regex = phrase + else + regex = /#{Regexp.escape(phrase)}/i + end + + return unless matches = text.match(regex) + phrase = matches[0] + + unless separator.empty? + text.split(separator).each do |value| + if value.match(regex) + regex = phrase = value + break + end + end + end + + first_part, second_part = text.split(phrase, 2) + + prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) + postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) + + affix = [first_part, separator, phrase, separator, second_part].join.strip + [prefix, affix, postfix].join + end + + private + + def cut_excerpt_part(part_position, part, separator, options) + return "", "" unless part + + radius = options.fetch(:radius, 100) + omission = options.fetch(:omission, "...") + + part = part.split(separator) + part.delete("") + affix = part.size > radius ? omission : "" + + part = if part_position == :first + drop_index = [part.length - radius, 0].max + part.drop(drop_index) + else + part.first(radius) + end + + return affix, part.join(separator) + end +end diff --git a/app/helpers/link_to_helper.rb b/app/helpers/link_to_helper.rb index dd6ffa805..3709469cf 100755 --- a/app/helpers/link_to_helper.rb +++ b/app/helpers/link_to_helper.rb @@ -28,19 +28,19 @@ module LinkToHelper # Incoming / outgoing messages def incoming_message_url(incoming_message, options = {}) - return request_url(incoming_message.info_request, options.merge(:anchor => "incoming-#{incoming_message.id}")) + message_url(incoming_message, options) end def incoming_message_path(incoming_message) - incoming_message_url(incoming_message, :only_path => true) + message_path(incoming_message) end def outgoing_message_url(outgoing_message, options = {}) - request_url(outgoing_message.info_request, options.merge(:anchor => "outgoing-#{outgoing_message.id}")) + message_url(outgoing_message, options) end def outgoing_message_path(outgoing_message) - outgoing_message_url(outgoing_message, :only_path => true) + message_path(outgoing_message) end def comment_url(comment, options = {}) @@ -279,73 +279,30 @@ module LinkToHelper end end - # Public: Usually-correct format for a DateTime-ish object - # To define a new new format define the `simple_date_{FORMAT}` method - # - # date - a DateTime, Date or Time - # opts - a Hash of options (default: { format: :html}) - # :format - :html returns a HTML <time> tag - # :text returns a plain String - # - # Examples - # - # simple_date(Time.now) - # # => "<time>..." - # - # simple_date(Time.now, :format => :text) - # # => "March 10, 2014" - # - # Returns a String - # Raises ArgumentError if the format is unrecognized - def simple_date(date, opts = {}) - opts = { :format => :html }.merge(opts) - date_formatter = "simple_date_#{ opts[:format] }" - - if respond_to?(date_formatter) - send(date_formatter, date) - else - raise ArgumentError, "Unrecognised format :#{ opts[:format] }" - end - end + #I18n locale switcher - # Usually-correct HTML formatting of a DateTime-ish object - # Use LinkToHelper#simple_date with desired formatting options - # - # date - a DateTime, Date or Time - # - # Returns a String - def simple_date_html(date) - date = date.in_time_zone unless date.is_a? Date - time_tag date, simple_date_text(date), :title => date.to_s + def locale_switcher(locale, params) + params['locale'] = locale + return url_for(params) end - # Usually-correct plain text formatting of a DateTime-ish object - # Use LinkToHelper#simple_date with desired formatting options - # - # date - a DateTime, Date or Time - # - # Returns a String - def simple_date_text(date) - date = date.in_time_zone.to_date unless date.is_a? Date + private - date_format = _("simple_date_format") - date_format = :long if date_format == "simple_date_format" - I18n.l(date, :format => date_format) - end + # Private: Generate a request_url linking to the new correspondence + def message_url(message, options = {}) + message_type = message.class.to_s.gsub('Message', '').downcase - def simple_time(date) - return date.strftime("%H:%M:%S").strip - end + default_options = { :anchor => "#{ message_type }-#{ message.id }" } - def year_from_date(date) - return date.strftime("%Y").strip - end + if options.delete(:cachebust) + default_options.merge!(:nocache => "#{ message_type }-#{ message.id }") + end - #I18n locale switcher + request_url(message.info_request, options.merge(default_options)) + end - def locale_switcher(locale, params) - params['locale'] = locale - return url_for(params) + def message_path(message) + message_url(message, :only_path => true) end end diff --git a/app/mailers/outgoing_mailer.rb b/app/mailers/outgoing_mailer.rb index 083c05a7c..797bf9fdd 100644 --- a/app/mailers/outgoing_mailer.rb +++ b/app/mailers/outgoing_mailer.rb @@ -8,7 +8,7 @@ # separated) paragraphs, as is the convention for all the other mailers. This # turned out to fit better with user exepectations when formatting messages. # -# XXX The other mail templates are written to use blank line separated +# TODO: The other mail templates are written to use blank line separated # paragraphs. They could be rewritten, and the wrapping method made uniform # throughout the application. @@ -35,10 +35,10 @@ class OutgoingMailer < ApplicationMailer :subject => OutgoingMailer.subject_for_followup(info_request, outgoing_message)) end - # XXX the condition checking valid_to_reply_to? also appears in views/request/_followup.html.erb, + # TODO: the condition checking valid_to_reply_to? also appears in views/request/_followup.html.erb, # it shouldn't really, should call something here. - # XXX also OutgoingMessage.get_salutation - # XXX these look like they should be members of IncomingMessage, but logically they + # TODO: also OutgoingMessage.get_salutation + # TODO: these look like they should be members of IncomingMessage, but logically they # need to work even when IncomingMessage is nil def OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup) if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? diff --git a/app/mailers/request_mailer.rb b/app/mailers/request_mailer.rb index 1fd5b9ba7..768257ba8 100644 --- a/app/mailers/request_mailer.rb +++ b/app/mailers/request_mailer.rb @@ -71,7 +71,7 @@ class RequestMailer < ApplicationMailer def new_response(info_request, incoming_message) # Don't use login link here, just send actual URL. This is # because people tend to forward these emails amongst themselves. - @url = incoming_message_url(incoming_message) + @url = incoming_message_url(incoming_message, :cachebust => true) @incoming_message, @info_request = incoming_message, info_request headers('Return-Path' => blackhole_email, @@ -234,7 +234,7 @@ class RequestMailer < ApplicationMailer def requests_matching_email(email) # We deliberately don't use Envelope-to here, so ones that are BCC # drop into the holding pen for checking. - reply_info_requests = [] # XXX should be set? + reply_info_requests = [] # TODO: should be set? for address in (email.to || []) + (email.cc || []) reply_info_request = InfoRequest.find_by_incoming_email(address) reply_info_requests.push(reply_info_request) if reply_info_request @@ -362,7 +362,7 @@ class RequestMailer < ApplicationMailer store_sent.user = info_request.user store_sent.alert_type = type_code store_sent.info_request_event_id = alert_event_id - # XXX uses same template for reminder 1 and reminder 2 right now. + # TODO: uses same template for reminder 1 and reminder 2 right now. RequestMailer.new_response_reminder_alert(info_request, last_response_message).deliver store_sent.save! end @@ -405,7 +405,7 @@ class RequestMailer < ApplicationMailer # cron jobs broke for more than a month events would be lost, but no # matter. I suspect the performance gain will be needed (with an index on updated_at) - # XXX the :order part info_request_events.created_at is a work around + # TODO: the :order part info_request_events.created_at is a work around # for a very old Rails bug which means eager loading does not respect # association orders. # http://dev.rubyonrails.org/ticket/3438 diff --git a/app/models/comment.rb b/app/models/comment.rb index b4c099123..a62c086d5 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -63,7 +63,7 @@ class Comment < ActiveRecord::Base # When posting a new comment, use this to check user hasn't double submitted. def Comment.find_existing(info_request_id, body) - # XXX can add other databases here which have regexp_replace + # TODO: can add other databases here which have regexp_replace if ActiveRecord::Base.connection.adapter_name == "PostgreSQL" # Exclude spaces from the body comparison using regexp_replace return Comment.find(:first, :conditions => [ "info_request_id = ? and regexp_replace(body, '[[:space:]]', '', 'g') = regexp_replace(?, '[[:space:]]', '', 'g')", info_request_id, body ]) diff --git a/app/models/foi_attachment.rb b/app/models/foi_attachment.rb index 6f198249a..a8d105f52 100644 --- a/app/models/foi_attachment.rb +++ b/app/models/foi_attachment.rb @@ -178,7 +178,7 @@ class FoiAttachment < ActiveRecord::Base return filename end - # XXX changing this will break existing URLs, so have a care - maybe + # TODO: changing this will break existing URLs, so have a care - maybe # make another old_display_filename see above def display_filename filename = self.filename diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index 124db8d4a..135a6bdaf 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -150,7 +150,7 @@ class IncomingMessage < ActiveRecord::Base end # The cached fields mentioned in the previous comment - # XXX there must be a nicer way to do this without all that + # TODO: there must be a nicer way to do this without all that # repetition. I tried overriding method_missing but got some # unpredictable results. def valid_to_reply_to @@ -194,7 +194,7 @@ class IncomingMessage < ActiveRecord::Base end # And look up by URL part number and display filename to get an attachment - # XXX relies on extract_attachments calling MailHandler.ensure_parts_counted + # TODO: relies on extract_attachments calling MailHandler.ensure_parts_counted # The filename here is passed from the URL parameter, so it's the # display_filename rather than the real filename. def self.get_attachment_by_url_part_number_and_filename(attachments, found_url_part_number, display_filename) @@ -220,7 +220,7 @@ class IncomingMessage < ActiveRecord::Base # Converts email addresses we know about into textual descriptions of them def mask_special_emails!(text) - # XXX can later display some of these special emails as actual emails, + # TODO: can later display some of these special emails as actual emails, # if they are public anyway. For now just be precautionary and only # put in descriptions of them in square brackets. if self.info_request.public_body.is_followupable? @@ -368,8 +368,8 @@ class IncomingMessage < ActiveRecord::Base # Remove quoted sections from emails (eventually the aim would be for this - # to do as good a job as GMail does) XXX bet it needs a proper parser - # XXX and this FOLDED_QUOTED_SECTION stuff is a mess + # to do as good a job as GMail does) TODO: bet it needs a proper parser + # TODO: and this FOLDED_QUOTED_SECTION stuff is a mess def self.remove_quoted_sections(text, replacement = "FOLDED_QUOTED_SECTION") text = text.dup replacement = "\n" + replacement + "\n" @@ -399,7 +399,7 @@ class IncomingMessage < ActiveRecord::Base ( \s*#{score}\n(?:(?!#{score}\n).)*? # top line (disclaimer:\n|confidential|received\sthis\semail\sin\serror|virus|intended\s+recipient|monitored\s+centrally|intended\s+(for\s+|only\s+for\s+use\s+by\s+)the\s+addressee|routinely\s+monitored|MessageLabs|unauthorised\s+use) - .*?(?:#{score}|\z) # bottom line OR end of whole string (for ones with no terminator XXX risky) + .*?(?:#{score}|\z) # bottom line OR end of whole string (for ones with no terminator TODO: risky) ) /imx, replacement) end @@ -480,7 +480,7 @@ class IncomingMessage < ActiveRecord::Base # Returns body text from main text part of email, converted to UTF-8, with uudecode removed, # emails and privacy sensitive things remove, censored, and folded to remove excess quoted text # (marked with FOLDED_QUOTED_SECTION) - # XXX returns a .dup of the text, so calling functions can in place modify it + # TODO: returns a .dup of the text, so calling functions can in place modify it def get_main_body_text_folded if self.cached_main_body_text_folded.nil? self._cache_main_body_text @@ -511,7 +511,7 @@ class IncomingMessage < ActiveRecord::Base source_charset = part.charset if part.content_type == 'text/html' # e.g. http://www.whatdotheyknow.com/request/35/response/177 - # XXX This is a bit of a hack as it is calling a + # TODO: This is a bit of a hack as it is calling a # convert to text routine. Could instead call a # sanitize HTML one. @@ -627,7 +627,7 @@ class IncomingMessage < ActiveRecord::Base return nil end # otherwise return it assuming it is text (sometimes you get things - # like binary/octet-stream, or the like, which are really text - XXX if + # like binary/octet-stream, or the like, which are really text - TODO: if # you find an example, put URL here - perhaps we should be always returning # nil in this case) return p @@ -722,7 +722,7 @@ class IncomingMessage < ActiveRecord::Base text = get_main_body_text_unfolded folded_quoted_text = get_main_body_text_folded - # Remove quoted sections, adding HTML. XXX The FOLDED_QUOTED_SECTION is + # Remove quoted sections, adding HTML. TODO: The FOLDED_QUOTED_SECTION is # a nasty hack so we can escape other HTML before adding the unfold # links, without escaping them. Rather than using some proper parser # making a tree structure (I don't know of one that is to hand, that diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 47ad435cb..aed651ad3 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -115,6 +115,16 @@ class InfoRequest < ActiveRecord::Base states end + # Subset of states accepted via the API + def self.allowed_incoming_states + [ + 'waiting_response', + 'rejected', + 'successful', + 'partially_successful' + ] + end + # Possible reasons that a request could be reported for administrator attention def report_reasons [_("Contains defamatory material"), @@ -387,16 +397,16 @@ public # When constructing a new request, use this to check user hasn't double submitted. - # XXX could have a date range here, so say only check last month's worth of new requests. If somebody is making + # TODO: could have a date range here, so say only check last month's worth of new requests. If somebody is making # repeated requests, say once a quarter for time information, then might need to do that. - # XXX this *should* also check outgoing message joined to is an initial + # TODO: this *should* also check outgoing message joined to is an initial # request (rather than follow up) def InfoRequest.find_existing(title, public_body_id, body) return InfoRequest.find(:first, :conditions => [ "title = ? and public_body_id = ? and outgoing_messages.body = ?", title, public_body_id, body ], :include => [ :outgoing_messages ] ) end def find_existing_outgoing_message(body) - # XXX can add other databases here which have regexp_replace + # TODO: can add other databases here which have regexp_replace if ActiveRecord::Base.connection.adapter_name == "PostgreSQL" # Exclude spaces from the body comparison using regexp_replace return self.outgoing_messages.find(:first, :conditions => [ "regexp_replace(outgoing_messages.body, '[[:space:]]', '', 'g') = regexp_replace(?, '[[:space:]]', '', 'g')", body ]) @@ -658,7 +668,7 @@ public event.last_described_at = Time.now() event.save! end - if event.last_described_at.nil? # XXX actually maybe this isn't needed + if event.last_described_at.nil? # TODO: actually maybe this isn't needed event.last_described_at = Time.now() event.save! end @@ -713,7 +723,7 @@ public elsif event.event_type == 'resent' last_sent = event elsif expecting_clarification and event.event_type == 'followup_sent' - # XXX this needs to cope with followup_resent, which it doesn't. + # TODO: this needs to cope with followup_resent, which it doesn't. # Not really easy to do, and only affects cases where followups # were resent after a clarification. last_sent = event diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index 5eed5ba83..9dde3ba80 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -75,7 +75,7 @@ class InfoRequestEvent < ActiveRecord::Base :values => [ [ :created_at, 0, "range_search", :date ], # for QueryParser range searches e.g. 01/01/2008..14/01/2008 [ :created_at_numeric, 1, "created_at", :number ], # for sorting - [ :described_at_numeric, 2, "described_at", :number ], # XXX using :number for lack of :datetime support in Xapian values + [ :described_at_numeric, 2, "described_at", :number ], # TODO: using :number for lack of :datetime support in Xapian values [ :request, 3, "request_collapse", :string ], [ :request_title_collapse, 4, "request_title_collapse", :string ], ], @@ -174,7 +174,7 @@ class InfoRequestEvent < ActiveRecord::Base end def get_clipped_response_efficiently - # XXX this ugly code is an attempt to not always load all the + # TODO: this ugly code is an attempt to not always load all the # columns for an incoming message, which can be *very* large # (due to all the cached text). We care particularly in this # case because it's called for every search result on a page @@ -266,7 +266,7 @@ class InfoRequestEvent < ActiveRecord::Base # We store YAML version of parameters in the database def params=(params) - # XXX should really set these explicitly, and stop storing them in + # TODO: should really set these explicitly, and stop storing them in # here, but keep it for compatibility with old way for now if not params[:incoming_message_id].nil? self.incoming_message_id = params[:incoming_message_id] @@ -392,7 +392,7 @@ class InfoRequestEvent < ActiveRecord::Base :outgoing_message_id => self.outgoing_message_id, :comment_id => self.comment_id, - # XXX would be nice to add links here, but alas the + # TODO: would be nice to add links here, but alas the # code to make them is in views only. See views/request/details.html.erb # perhaps can call with @template somehow } diff --git a/app/models/mail_server_log.rb b/app/models/mail_server_log.rb index 0e5b60ff1..07d2fdac0 100644 --- a/app/models/mail_server_log.rb +++ b/app/models/mail_server_log.rb @@ -166,7 +166,7 @@ class MailServerLog < ActiveRecord::Base # lines. Writes any errors to STDERR. This check is really mainly to # check the envelope from is the request address, as Ruby is quite # flaky with regard to that, and it is important for anti-spam reasons. - # XXX does this really check that, as the log just wouldn't pick + # TODO: does this really check that, as the log just wouldn't pick # up at all if the requests weren't sent that way as there would be # no request- email in it? # diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index a435511d3..160f69d0b 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -125,7 +125,7 @@ class OutgoingMessage < ActiveRecord::Base get_salutation + "\n\n" + get_default_letter + "\n\n" + get_signoff + "\n\n" end def set_signature_name(name) - # XXX We use raw_body here to get unstripped one + # TODO: We use raw_body here to get unstripped one if self.raw_body == self.get_default_message self.body = self.raw_body + name end diff --git a/app/models/post_redirect.rb b/app/models/post_redirect.rb index 5da3d2742..6f288b471 100644 --- a/app/models/post_redirect.rb +++ b/app/models/post_redirect.rb @@ -65,7 +65,7 @@ class PostRedirect < ActiveRecord::Base # Used by (rspec) test code only def self.get_last_post_redirect - # XXX yeuch - no other easy way of getting the token so we can check + # TODO: yeuch - no other easy way of getting the token so we can check # the redirect URL, as it is by definition opaque to the controller # apart from in the place that it redirects to. post_redirects = PostRedirect.find_by_sql("select * from post_redirects order by id desc limit 1") diff --git a/app/models/profile_photo.rb b/app/models/profile_photo.rb index 6c3b2cfa0..3c0be222c 100644 --- a/app/models/profile_photo.rb +++ b/app/models/profile_photo.rb @@ -115,7 +115,7 @@ class ProfilePhoto < ActiveRecord::Base return end - self.image = image_list[0] # XXX perhaps take largest image or somesuch if there were multiple in the file? + self.image = image_list[0] # TODO: perhaps take largest image or somesuch if there were multiple in the file? self.convert_image end end diff --git a/app/models/public_body.rb b/app/models/public_body.rb index 03ec270ee..b22482541 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -93,7 +93,7 @@ class PublicBody < ActiveRecord::Base self.translations.find_by_locale(locale) end - # XXX - Don't like repeating this! + # TODO: - Don't like repeating this! def calculate_cached_fields(t) PublicBody.set_first_letter(t) short_long_name = t.name @@ -329,7 +329,7 @@ class PublicBody < ActiveRecord::Base first = false end if html - # XXX this should call proper route helpers, but is in model sigh + # TODO: this should call proper route helpers, but is in model sigh desc = '<a href="/body/list/' + tag.name + '">' + desc + '</a>' end types.push(desc) @@ -399,6 +399,9 @@ class PublicBody < ActiveRecord::Base end end + def site_administration? + has_tag?('site_administration') + end class ImportCSVDryRun < StandardError end @@ -569,45 +572,6 @@ class PublicBody < ActiveRecord::Base return [errors, notes] end - # Returns all public bodies (except for the internal admin authority) as csv - def self.export_csv(output_filename) - CSV.open(output_filename, "w") do |csv| - csv << [ - 'Name', - 'Short name', - # deliberately not including 'Request email' - 'URL name', - 'Tags', - 'Home page', - 'Publication scheme', - 'Disclosure log', - 'Notes', - 'Created at', - 'Updated at', - 'Version', - ] - PublicBody.visible.find_each(:include => [:translations, :tags]) do |public_body| - # Skip bodies we use only for site admin - next if public_body.has_tag?('site_administration') - csv << [ - public_body.name, - public_body.short_name, - # DO NOT include request_email (we don't want to make it - # easy to spam all authorities with requests) - public_body.url_name, - public_body.tag_string, - public_body.calculated_home_page, - public_body.publication_scheme, - public_body.disclosure_log, - public_body.notes, - public_body.created_at, - public_body.updated_at, - public_body.version, - ] - end - end - end - # Does this user have the power of FOI officer for this body? def is_foi_officer?(user) user_domain = user.email_domain diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb index 13b6f78dd..10ba28f4a 100644 --- a/app/models/track_thing.rb +++ b/app/models/track_thing.rb @@ -149,7 +149,7 @@ class TrackThing < ActiveRecord::Base end end track_thing.track_query = query - # XXX should extract requested_by:, request:, requested_from: + # TODO: should extract requested_by:, request:, requested_from: # and stick their values into the respective relations. # Should also update "params" to make the list_description # nicer and more generic. It will need to do some clever @@ -271,7 +271,7 @@ class TrackThing < ActiveRecord::Base :web => _("To follow requests and responses matching your search"), :email => _("Then you will be notified whenever a new request or response matches your search."), :email_subject => _("Confirm you want to follow new requests or responses matching your search"), - # RSS sorting - XXX hmmm, we don't really know which to use + # RSS sorting - TODO: hmmm, we don't really know which to use # here for sorting. Might be a query term (e.g. 'cctv'), in # which case newest is good, or might be something like # all refused requests in which case want to sort by diff --git a/app/models/user.rb b/app/models/user.rb index d75622b37..4b83d8572 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,7 +99,7 @@ class User < ActiveRecord::Base end # Don't display any leading/trailing spaces - # XXX we have strip_attributes! now, so perhaps this can be removed (might + # TODO: we have strip_attributes! now, so perhaps this can be removed (might # be still needed for existing cases) def name name = read_attribute(:name) @@ -222,7 +222,7 @@ class User < ActiveRecord::Base # Can the user make new requests, without having to describe state of (most) existing ones? def can_leave_requests_undescribed? - # XXX should be flag in database really + # TODO: should be flag in database really if self.url_name == "heather_brooke" || self.url_name == "heather_brooke_2" return true end @@ -425,7 +425,7 @@ class User < ActiveRecord::Base ## Class methods def User.encrypted_password(password, salt) - string_to_hash = password + salt # XXX need to add a secret here too? + string_to_hash = password + salt # TODO: need to add a secret here too? Digest::SHA1.hexdigest(string_to_hash) end diff --git a/app/views/admin_general/index.html.erb b/app/views/admin_general/index.html.erb index 2202663be..f29258162 100644 --- a/app/views/admin_general/index.html.erb +++ b/app/views/admin_general/index.html.erb @@ -39,7 +39,7 @@ <% if message.get_body_for_quoting.strip.size == 0 %> <%= link_to "(no body)", admin_request_show_raw_email_path(message.raw_email_id) %> <% else %> - <%= link_to excerpt(message.get_body_for_quoting, "", 60), admin_request_show_raw_email_path(message.raw_email_id) %> + <%= link_to excerpt(message.get_body_for_quoting, "", :radius => 60), admin_request_show_raw_email_path(message.raw_email_id) %> <% end %> </td> <td class="span2"> diff --git a/app/views/admin_request/edit.html.erb b/app/views/admin_request/edit.html.erb index 0e9c68aea..552b31bbb 100644 --- a/app/views/admin_request/edit.html.erb +++ b/app/views/admin_request/edit.html.erb @@ -33,7 +33,7 @@ <p><label for="info_request_tag_string"><strong>Tags</strong> <small>(space separated, can use key:value)</small></label><br/> <%= text_field 'info_request', 'tag_string', :size => 60 %></p> - <p><%= submit_tag 'Save changes', :accesskey => 's' %> + <p><%= submit_tag 'Save changes', :accesskey => 's', :class => 'btn btn-primary' %> </p> <p><strong>Note:</strong> To edit the actual request body text, click edit @@ -49,13 +49,17 @@ <hr> +<div class="well"> + <%= form_tag admin_request_destroy_path(@info_request) do %> <p> - <strong>This is permanent and irreversible!</strong> <%= submit_tag 'Destroy request entirely' %> + <strong>This is permanent and irreversible!</strong> <%= submit_tag 'Destroy request entirely', :class => 'btn btn-danger' %> <br>Use it mainly if someone posts private information, e.g. made a Data Protection request. It destroys all responses and tracks as well. </p> <% end %> +</div> + <hr> diff --git a/app/views/general/_frontpage_bodies_list.html.erb b/app/views/general/_frontpage_bodies_list.html.erb index 44321f14a..1c35c55ad 100644 --- a/app/views/general/_frontpage_bodies_list.html.erb +++ b/app/views/general/_frontpage_bodies_list.html.erb @@ -3,9 +3,10 @@ <div id="examples_0"> <h3><%= _("Who can I request information from?") %></h3> <%= _("{{site_name}} covers requests to {{number_of_authorities}} authorities, including:", - :site_name => site_name, :number_of_authorities => PublicBody.visible.count) %> + :site_name => site_name, + :number_of_authorities => number_with_delimiter(PublicBody.visible.count)) %> <ul> - <% for popular_body in popular_bodies %> + <% popular_bodies.each do |popular_body| %> <li><%=public_body_link(popular_body)%> <%= n_('{{count}} request', '{{count}} requests', popular_body.info_requests_count, :count => popular_body.info_requests_count) %> </li> diff --git a/app/views/general/_frontpage_new_request.html.erb b/app/views/general/_frontpage_new_request.html.erb index 499b60eb5..5b987b320 100644 --- a/app/views/general/_frontpage_new_request.html.erb +++ b/app/views/general/_frontpage_new_request.html.erb @@ -4,4 +4,4 @@ Information<br/> request</strong>") %> </h1> -<a class="link_button_green_large" href="<%= select_authority_path %>"><%= _("Start now »") %></a> +<a class="link_button_green_large" href="<%= select_authority_path %>"><%= _("Make a request »") %></a> diff --git a/app/views/general/_frontpage_requests_list.html.erb b/app/views/general/_frontpage_requests_list.html.erb index d7d9184c4..39187f3f0 100644 --- a/app/views/general/_frontpage_requests_list.html.erb +++ b/app/views/general/_frontpage_requests_list.html.erb @@ -8,9 +8,10 @@ <% end %> </h3> <%= _("{{site_name}} users have made {{number_of_requests}} requests, including:", - :site_name => site_name, :number_of_requests => InfoRequest.visible.count) %> + :site_name => site_name, + :number_of_requests => number_with_delimiter(InfoRequest.visible.count)) %> <ul> - <% for event in @request_events %> + <% @request_events.each do |event| %> <li> <% if @request_events_all_successful %> <%= _("{{public_body_link}} answered a request about", diff --git a/app/views/general/_frontpage_search_box.html.erb b/app/views/general/_frontpage_search_box.html.erb index 890602416..f77bd97fc 100644 --- a/app/views/general/_frontpage_search_box.html.erb +++ b/app/views/general/_frontpage_search_box.html.erb @@ -2,7 +2,8 @@ <%= _("Search over<br/> <strong>{{number_of_requests}} requests</strong> <span>and</span><br/> <strong>{{number_of_authorities}} authorities</strong>", - :number_of_requests => InfoRequest.visible.count, :number_of_authorities => PublicBody.visible.count) %> + :number_of_requests => number_with_delimiter(InfoRequest.visible.count), + :number_of_authorities => number_with_delimiter(PublicBody.visible.count)) %> </h2> <form id="search_form" method="post" action="<%= search_redirect_path %>"> <div> diff --git a/app/views/general/_locale_switcher.html.erb b/app/views/general/_locale_switcher.html.erb index a318f61f3..7b6377665 100644 --- a/app/views/general/_locale_switcher.html.erb +++ b/app/views/general/_locale_switcher.html.erb @@ -1,7 +1,7 @@ <% if FastGettext.default_available_locales.length > 1 && !params.empty? %> <div id="user_locale_switcher"> <div class="btn-group"> - <% for possible_locale in FastGettext.default_available_locales %> + <% FastGettext.default_available_locales.each do |possible_locale| %> <% if possible_locale == FastGettext.locale %> <a href="#" class="btn disabled"><%= locale_name(possible_locale) %></a> <% else %> diff --git a/app/views/general/_responsive_topnav.html.erb b/app/views/general/_responsive_topnav.html.erb index e726c17f4..0ece0da9a 100644 --- a/app/views/general/_responsive_topnav.html.erb +++ b/app/views/general/_responsive_topnav.html.erb @@ -6,7 +6,7 @@ </li> <li class="<%= 'selected' if params[:controller] == 'request' and !['new', 'select_authority'].include?(params[:action]) %>"> - <%= link_to _("View requests"), request_list_successful_path %> + <%= link_to _("Browse requests"), request_list_successful_path %> </li> <li class="<%= 'selected' if params[:controller] == 'public_body' %>"> diff --git a/app/views/general/_topnav.html.erb b/app/views/general/_topnav.html.erb index d37bd97d1..04ca07fa9 100644 --- a/app/views/general/_topnav.html.erb +++ b/app/views/general/_topnav.html.erb @@ -2,7 +2,9 @@ <ul id="navigation"> <li class="<%= 'selected' if params[:controller] == 'general' and params[:action] != 'blog' and params[:action] != 'search' %>"><%= link_to _("Home"), frontpage_path %></li> <li class="<%= 'selected' if params[:controller] == 'request' and ['new', 'select_authority'].include?(params[:action]) %>"><%= link_to _("Make a request"), select_authority_path, :id => 'make-request-link' %></li> - <li class="<%= 'selected' if params[:controller] == 'request' and !['new', 'select_authority'].include?(params[:action]) %>"><%= link_to _("View requests"), request_list_successful_path %></li> + <li class="<%= 'selected' if params[:controller] == 'request' and !['new', 'select_authority'].include?(params[:action]) %>"> + <%= link_to _("Browse requests"), request_list_successful_path %> + </li> <li class="<%= 'selected' if params[:controller] == 'public_body' %>"><%= link_to _("View authorities"), list_public_bodies_default_path %></li> <% unless AlaveteliConfiguration::blog_feed.empty? %> <li class="<%= 'selected' if params[:controller] == 'general' and params[:action] == 'blog' %>"><%= link_to _("Read blog"), blog_path %></li> diff --git a/app/views/public_body/_list_sidebar_extra.html.erb b/app/views/public_body/_list_sidebar_extra.html.erb index 290593d6a..6e683d7a1 100644 --- a/app/views/public_body/_list_sidebar_extra.html.erb +++ b/app/views/public_body/_list_sidebar_extra.html.erb @@ -1,3 +1,8 @@ +<% if AlaveteliConfiguration::public_body_statistics_page %> + <p> + <%= link_to _('Public authority statistics'), public_bodies_statistics_path %> + </p> +<% end %> <p> <%= link_to _('Are we missing a public authority?'), help_requesting_path + '#missing_body' %> </p> diff --git a/app/views/public_body/show.html.erb b/app/views/public_body/show.html.erb index a9c50e657..9352747ea 100644 --- a/app/views/public_body/show.html.erb +++ b/app/views/public_body/show.html.erb @@ -127,7 +127,10 @@ <% if @xapian_requests.results.empty? %> <p><% _('There were no requests matching your query.') %></p> <% else %> - <p> <%= _('Only requests made using {{site_name}} are shown.', :site_name => site_name) %></p> + <p> + <%= _('Only requests made using {{site_name}} are shown.', :site_name => site_name) %> + <%= link_to _('?'), help_about_path %> + </p> <% end %> <% else %> diff --git a/app/views/public_body/statistics.html.erb b/app/views/public_body/statistics.html.erb index d935a9e47..0e7e7424e 100644 --- a/app/views/public_body/statistics.html.erb +++ b/app/views/public_body/statistics.html.erb @@ -1,6 +1,6 @@ <% @title = _("Public Body Statistics") %> <div id="main_content"> - <h1>Public Body Statistics</h1> + <h1><%= @title %></h1> <p><%= _("This page of public body statistics is currently \ experimental, so there are some caveats that should be borne \ @@ -33,6 +33,8 @@ requests to the authority through this site, rather than, \ say, all requests that have been made to the public body by \ any means.") %></li> + <li><%= _("Unclassified or hidden requests are not counted.") %></li> + </ul> <p><%= _("These graphs were partly inspired by \ @@ -47,7 +49,7 @@ are due to him.") %></p> <table border=0> <thead> <tr> - <th>Public Body</th> + <th><%= _('Public Body') %></th> <th><%= graph_data['y_axis'] %></th> </tr> </thead> diff --git a/app/views/public_body_change_requests/new.html.erb b/app/views/public_body_change_requests/new.html.erb index 7079cd868..b52d583be 100644 --- a/app/views/public_body_change_requests/new.html.erb +++ b/app/views/public_body_change_requests/new.html.erb @@ -54,6 +54,11 @@ <%= f.text_area :notes, :rows => 10, :cols => 60 %> </p> + <p style="display:none;"> + <%= label_tag 'public_body_change_request[comment]', _('Do not fill in this field') %> + <%= text_field_tag 'public_body_change_request[comment]' %> + </p> + <div class="form_button"> <%= submit_tag _("Submit request") %> </div> diff --git a/app/views/request/_sidebar_request_listing.html.erb b/app/views/request/_sidebar_request_listing.html.erb index ec5a5813d..64fe39341 100644 --- a/app/views/request/_sidebar_request_listing.html.erb +++ b/app/views/request/_sidebar_request_listing.html.erb @@ -4,7 +4,7 @@ <%= request_link(info_request) %> </span> <span class="desc"> - <%=h excerpt(info_request.initial_request_text, "", 100) %> + <%=h excerpt(info_request.initial_request_text, "", :radius => 100) %> </span> <span class="bottomline"> <strong> diff --git a/app/views/request/_view_html_stylesheet.html.erb b/app/views/request/_view_html_stylesheet.html.erb index f3d8799da..09f295346 100644 --- a/app/views/request/_view_html_stylesheet.html.erb +++ b/app/views/request/_view_html_stylesheet.html.erb @@ -1,17 +1,16 @@ <% if AlaveteliConfiguration::responsive_styling || params[:responsive] %> <!--[if LTE IE 7]> - <link href="/assets/responsive/application-lte-ie7.css" media="all" rel="stylesheet" title="Main" type="text/css" /> + <link href="/assets/responsive/application-lte-ie7.css" media="all" rel="stylesheet" title="Main" type="text/css" charset="UTF-8" /> <![endif]--> <!--[if IE 8]> - <link href="/assets/responsive/application-ie8.css" media="all" rel="stylesheet" title="Main" type="text/css" /> + <link href="/assets/responsive/application-ie8.css" media="all" rel="stylesheet" title="Main" type="text/css" charset="UTF-8" /> <![endif]--> <!--[if GT IE 8]><!--> - <link href="/assets/responsive/application.css" media="all" rel="stylesheet" title="Main" type="text/css" /> + <link href="/assets/responsive/application.css" media="all" rel="stylesheet" title="Main" type="text/css" charset="UTF-8" /> <!--<![endif]--> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <% else %> - <link type="text/css" title="Main" rel="stylesheet" media="screen" href="/assets/application.css"> - + <link type="text/css" title="Main" rel="stylesheet" media="screen" href="/assets/application.css" charset="UTF-8" /> <% end %> diff --git a/app/views/request/upload_response.html.erb b/app/views/request/upload_response.html.erb index f5fd6f000..f07af34d5 100644 --- a/app/views/request/upload_response.html.erb +++ b/app/views/request/upload_response.html.erb @@ -9,7 +9,9 @@ <%= foi_error_messages_for :comment %> - <h1><%= _('Respond to the FOI request')%> '<%=request_link(@info_request)%>'<% _(' made by ')%><%=user_link(@info_request.user) %></h1> + <h1><%= _("Respond to the FOI request '{{request}}' made by {{user}}", + :request => request_link(@info_request), + :user => user_link(@info_request.user)) %></h1> <p> <%= raw(_('Your response will <strong>appear on the Internet</strong>, <a href="{{url}}">read why</a> and answers to other questions.', :url => help_officers_path.html_safe)) %> @@ -48,5 +50,3 @@ </p> <% end %> <% end %> - - diff --git a/app/views/track_mailer/event_digest.text.erb b/app/views/track_mailer/event_digest.text.erb index b83c184f0..f6e699e41 100644 --- a/app/views/track_mailer/event_digest.text.erb +++ b/app/views/track_mailer/event_digest.text.erb @@ -4,7 +4,7 @@ for track_thing, alert_results, xapian_object in @email_about_things main_text += track_thing.params[:title_in_email] + "\n" main_text += ("=" * track_thing.params[:title_in_email].size) + "\n\n" - @highlight_words = xapian_object.words_to_highlight + @highlight_words = xapian_object.words_to_highlight(:regex => true) for result in alert_results.reverse if result[:model].class.to_s == "InfoRequestEvent" event = result[:model] @@ -17,14 +17,14 @@ # e.g. Julian Burgess sent a request to Royal Mail Group (15 May 2008) if event.event_type == 'response' - url = incoming_message_url(event.incoming_message) + url = incoming_message_url(event.incoming_message, :cachebust => true) main_text += _("{{public_body}} sent a response to {{user_name}}", :public_body => event.info_request.public_body.name, :user_name => event.info_request.user_name) elsif event.event_type == 'followup_sent' - url = outgoing_message_url(event.outgoing_message) + url = outgoing_message_url(event.outgoing_message, :cachebust => true) main_text += _("{{user_name}} sent a follow up message to {{public_body}}", :user_name => event.info_request.user_name, :public_body => event.info_request.public_body.name) elsif event.event_type == 'sent' # this is unlikely to happen in real life, but happens in the test code - url = outgoing_message_url(event.outgoing_message) + url = outgoing_message_url(event.outgoing_message, :cachebust => true) main_text += _("{{user_name}} sent a request to {{public_body}}", :user_name => event.info_request.user_name, :public_body => event.info_request.public_body.name) elsif event.event_type == 'comment' url = comment_url(event.comment) diff --git a/app/views/user/_user_listing_single.html.erb b/app/views/user/_user_listing_single.html.erb index ed1b95718..3cb0d283f 100644 --- a/app/views/user/_user_listing_single.html.erb +++ b/app/views/user/_user_listing_single.html.erb @@ -18,7 +18,7 @@ end %> <span class="bottomline"> <%= pluralize(display_user.info_requests.size, "request") %> <%= _('made.')%> <%= pluralize(display_user.visible_comments.size, "annotation") %> <%= _('made.')%> - <%= _('Joined in')%> <%= year_from_date(display_user.created_at) %>. + <%= _('Joined in')%> <%= display_user.created_at.year %>. </span> </div> diff --git a/app/views/user/show.html.erb b/app/views/user/show.html.erb index ce328b46f..7ae577565 100644 --- a/app/views/user/show.html.erb +++ b/app/views/user/show.html.erb @@ -64,7 +64,7 @@ <h1> <%= h(@display_user.name) + (@is_you ? _(" (you)") : "") %></h1> <p class="subtitle"> - <%= _('Joined {{site_name}} in', :site_name=>site_name) %> <%= year_from_date(@display_user.created_at) %> + <%= _('Joined {{site_name}} in', :site_name=>site_name) %> <%= @display_user.created_at.year %> <% if !@user.nil? && @user.admin_page_links? %> (<%= link_to "admin", admin_user_show_path(@display_user) %>) <% end %> |