From: Tom Hughes Date: Sat, 13 Aug 2011 10:48:04 +0000 (+0100) Subject: Merge branch 'master' into openstreetbugs X-Git-Tag: live~5104^2~143 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/72e33046c80bddc0ca9b1336786dcb4b349137f7?hp=9c1a3e47086b53592aafa9452a2a666d5c972f0d Merge branch 'master' into openstreetbugs Conflicts: app/views/browse/_map.html.erb app/views/site/index.html.erb --- diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index a7dd5f5c9..2036e4f10 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -79,4 +79,13 @@ class BrowseController < ApplicationController rescue ActiveRecord::RecordNotFound render :action => "not_found", :status => :not_found end + + def note + @type = "note" + @note = Note.find(params[:id]) + @next = Note.find(:first, :order => "id ASC", :conditions => [ "status != 'hidden' AND id > :id", { :id => @note.id }] ) + @prev = Note.find(:first, :order => "id DESC", :conditions => [ "status != 'hidden' AND id < :id", { :id => @note.id }] ) + rescue ActiveRecord::RecordNotFound + render :action => "not_found", :status => :not_found + end end diff --git a/app/controllers/note_controller.rb b/app/controllers/note_controller.rb new file mode 100644 index 000000000..c46145045 --- /dev/null +++ b/app/controllers/note_controller.rb @@ -0,0 +1,387 @@ +class NoteController < ApplicationController + + layout 'site', :only => [:mine] + + before_filter :check_api_readable + before_filter :authorize_web, :only => [:create, :close, :update, :delete, :mine] + before_filter :check_api_writable, :only => [:create, :close, :update, :delete] + before_filter :set_locale, :only => [:mine] + after_filter :compress_output + around_filter :api_call_handle_error, :api_call_timeout + + # Help methods for checking boundary sanity and area size + include MapBoundary + + ## + # Return a list of notes in a given area + def list + # Figure out the bbox - we prefer a bbox argument but also + # support the old, deprecated, method with four arguments + if params[:bbox] + raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3 + + bbox = params[:bbox].split(",") + else + raise OSM::APIBadUserInput.new("No l was given") unless params[:l] + raise OSM::APIBadUserInput.new("No r was given") unless params[:r] + raise OSM::APIBadUserInput.new("No b was given") unless params[:b] + raise OSM::APIBadUserInput.new("No t was given") unless params[:t] + + bbox = [ params[:l], params[:b], params[:r], params[:t] ] + end + + # Get the sanitised boundaries + @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(bbox) + + # Get any conditions that need to be applied + conditions = closed_condition + + # Check that the boundaries are valid + check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA) + + # Find the notes we want to return + @notes = Note.find_by_area(@min_lat, @min_lon, @max_lat, @max_lon, + :include => :comments, + :conditions => conditions, + :order => "updated_at DESC", + :limit => result_limit) + + # Render the result + respond_to do |format| + format.html { render :format => :rjs, :content_type => "text/javascript" } + format.rss + format.js + format.xml + format.json { render :json => @notes.to_json } + format.gpx + end + end + + ## + # Create a new note + def create + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No lat was given") unless params[:lat] + raise OSM::APIBadUserInput.new("No lon was given") unless params[:lon] + raise OSM::APIBadUserInput.new("No text was given") unless params[:text] + + # Extract the arguments + lon = params[:lon].to_f + lat = params[:lat].to_f + comment = params[:text] + name = params[:name] + + # Include in a transaction to ensure that there is always a note_comment for every note + Note.transaction do + # Create the note + @note = Note.create(:lat => lat, :lon => lon) + raise OSM::APIBadUserInput.new("The note is outside this world") unless @note.in_world? + + #TODO: move this into a helper function + begin + url = "http://nominatim.openstreetmap.org/reverse?lat=" + lat.to_s + "&lon=" + lon.to_s + "&zoom=16" + response = REXML::Document.new(Net::HTTP.get(URI.parse(url))) + + if result = response.get_text("reversegeocode/result") + @note.nearby_place = result.to_s + else + @note.nearby_place = "unknown" + end + rescue Exception => err + @note.nearby_place = "unknown" + end + + # Save the note + @note.save + + # Add a comment to the note + add_comment(@note, comment, name, "opened") + end + + # Send an OK response + render_ok + end + + ## + # Add a comment to an existing note + def update + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No id was given") unless params[:id] + raise OSM::APIBadUserInput.new("No text was given") unless params[:text] + + # Extract the arguments + id = params[:id].to_i + comment = params[:text] + name = params[:name] or "NoName" + + # Find the note and check it is valid + note = Note.find(id) + raise OSM::APINotFoundError unless note + raise OSM::APIAlreadyDeletedError unless note.visible? + + # Add a comment to the note + Note.transaction do + add_comment(note, comment, name, "commented") + end + + # Send an OK response + render_ok + end + + ## + # Close a note + def close + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No id was given") unless params[:id] + + # Extract the arguments + id = params[:id].to_i + name = params[:name] + + # Find the note and check it is valid + note = Note.find_by_id(id) + raise OSM::APINotFoundError unless note + raise OSM::APIAlreadyDeletedError unless note.visible? + + # Close the note and add a comment + Note.transaction do + note.close + + add_comment(note, nil, name, "closed") + end + + # Send an OK response + render_ok + end + + ## + # Get a feed of recent notes and comments + def rss + # Get any conditions that need to be applied + conditions = closed_condition + + # Process any bbox + if params[:bbox] + raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3 + + @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(params[:bbox].split(',')) + + check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA) + + conditions = cond_merge conditions, [OSM.sql_for_area(@min_lat, @min_lon, @max_lat, @max_lon, "notes.")] + end + + # Find the comments we want to return + @comments = NoteComment.find(:all, + :conditions => conditions, + :order => "created_at DESC", + :limit => result_limit, + :joins => :note, + :include => :note) + + # Render the result + respond_to do |format| + format.rss + end + end + + ## + # Read a note + def read + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No id was given") unless params[:id] + + # Find the note and check it is valid + @note = Note.find(params[:id]) + raise OSM::APINotFoundError unless @note + raise OSM::APIAlreadyDeletedError unless @note.visible? + + # Render the result + respond_to do |format| + format.xml + format.rss + format.json { render :json => @note.to_json } + format.gpx + end + end + + ## + # Delete (hide) a note + def delete + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No id was given") unless params[:id] + + # Extract the arguments + id = params[:id].to_i + name = params[:name] + + # Find the note and check it is valid + note = Note.find(id) + raise OSM::APINotFoundError unless note + raise OSM::APIAlreadyDeletedError unless note.visible? + + # Mark the note as hidden + Note.transaction do + note.status = "hidden" + note.save + + add_comment(note, nil, name, "hidden") + end + + # Render the result + render :text => "ok\n", :content_type => "text/html" + end + + ## + # Return a list of notes matching a given string + def search + # Check the arguments are sane + raise OSM::APIBadUserInput.new("No query string was given") unless params[:q] + + # Get any conditions that need to be applied + conditions = closed_condition + conditions = cond_merge conditions, ['note_comments.body ~ ?', params[:q]] + + # Find the notes we want to return + @notes = Note.find(:all, + :conditions => conditions, + :order => "updated_at DESC", + :limit => result_limit, + :joins => :comments, + :include => :comments) + + # Render the result + respond_to do |format| + format.html { render :action => :list, :format => :rjs, :content_type => "text/javascript"} + format.rss { render :action => :list } + format.js + format.xml { render :action => :list } + format.json { render :json => @notes.to_json } + format.gpx { render :action => :list } + end + end + + def mine + if params[:display_name] + @user2 = User.find_by_display_name(params[:display_name], :conditions => { :status => ["active", "confirmed"] }) + + if @user2 + if @user2.data_public? or @user2 == @user + conditions = ['note_comments.author_id = ?', @user2.id] + else + conditions = ['false'] + end + else #if request.format == :html + @title = t 'user.no_such_user.title' + @not_found_user = params[:display_name] + render :template => 'user/no_such_user', :status => :not_found + return + end + end + + if @user2 + user_link = render_to_string :partial => "user", :object => @user2 + end + + @title = t 'note.mine.title', :user => @user2.display_name + @heading = t 'note.mine.heading', :user => @user2.display_name + @description = t 'note.mine.description', :user => user_link + + @page = (params[:page] || 1).to_i + @page_size = 10 + + @notes = Note.find(:all, + :include => [:comments, {:comments => :author}], + :joins => :comments, + :order => "updated_at DESC", + :conditions => conditions, + :offset => (@page - 1) * @page_size, + :limit => @page_size).uniq + end + +private + #------------------------------------------------------------ + # utility functions below. + #------------------------------------------------------------ + + ## + # merge two conditions + # TODO: this is a copy from changeset_controler.rb and should be factored out to share + def cond_merge(a, b) + if a and b + a_str = a.shift + b_str = b.shift + return [ a_str + " AND " + b_str ] + a + b + elsif a + return a + else b + return b + end + end + + ## + # Render an OK response + def render_ok + if params[:format] == "js" + render :text => "osbResponse();", :content_type => "text/javascript" + else + render :text => "ok " + @note.id.to_s + "\n", :content_type => "text/plain" if @note + render :text => "ok\n", :content_type => "text/plain" unless @note + end + end + + ## + # Get the maximum number of results to return + def result_limit + if params[:limit] and params[:limit].to_i > 0 and params[:limit].to_i < 10000 + params[:limit].to_i + else + 100 + end + end + + ## + # Generate a condition to choose which bugs we want based + # on their status and the user's request parameters + def closed_condition + if params[:closed] + closed_since = params[:closed].to_i + else + closed_since = 7 + end + + if closed_since < 0 + conditions = ["status != 'hidden'"] + elsif closed_since > 0 + conditions = ["(status = 'open' OR (status = 'closed' AND closed_at > '#{Time.now - closed_since.days}'))"] + else + conditions = ["status = 'open'"] + end + + return conditions + end + + ## + # Add a comment to a note + def add_comment(note, text, name, event) + name = "NoName" if name.nil? + + attributes = { :visible => true, :event => event, :body => text } + + if @user + attributes[:author_id] = @user.id + attributes[:author_name] = @user.display_name + else + attributes[:author_ip] = request.remote_ip + attributes[:author_name] = name + " (a)" + end + + note.comments.create(attributes) + + note.comments.map { |c| c.author }.uniq.each do |user| + if user and user != @user + Notifier.deliver_note_comment_notification(comment, user) + end + end + end +end diff --git a/app/models/note.rb b/app/models/note.rb new file mode 100644 index 000000000..892ada1aa --- /dev/null +++ b/app/models/note.rb @@ -0,0 +1,88 @@ +class Note < ActiveRecord::Base + include GeoRecord + + has_many :comments, :class_name => "NoteComment", + :foreign_key => :note_id, + :order => :created_at, + :conditions => { :visible => true } + + validates_presence_of :id, :on => :update + validates_uniqueness_of :id + validates_numericality_of :latitude, :only_integer => true + validates_numericality_of :longitude, :only_integer => true + validates_presence_of :closed_at if :status == "closed" + validates_inclusion_of :status, :in => ["open", "closed", "hidden"] + validate :validate_position + + # Sanity check the latitude and longitude and add an error if it's broken + def validate_position + errors.add_to_base("Note is not in the world") unless in_world? + end + + # Fill in default values for new notes + def after_initialize + self.status = "open" unless self.attribute_present?(:status) + end + + # Close a note + def close + self.status = "closed" + self.closed_at = Time.now.getutc + self.save + end + + # Return a flattened version of the comments for a note + def flatten_comment(separator_char, upto_timestamp = :nil) + resp = "" + comment_no = 1 + self.comments.each do |comment| + next if upto_timestamp != :nil and comment.created_at > upto_timestamp + resp += (comment_no == 1 ? "" : separator_char) + resp += comment.body if comment.body + resp += " [ " + resp += comment.author_name if comment.author_name + resp += " " + comment.created_at.to_s + " ]" + comment_no += 1 + end + + return resp + end + + # Check if a note is visible + def visible? + return status != "hidden" + end + + # Return the author object, derived from the first comment + def author + self.comments.first.author + end + + # Return the author IP address, derived from the first comment + def author_ip + self.comments.first.author_ip + end + + # Return the author id, derived from the first comment + def author_id + self.comments.first.author_id + end + + # Return the author name, derived from the first comment + def author_name + self.comments.first.author_name + end + + # Custom JSON output routine for notes + def to_json(options = {}) + super options.reverse_merge( + :methods => [ :lat, :lon ], + :only => [ :id, :status, :created_at ], + :include => { + :comments => { + :only => [ :event, :author_name, :created_at, :body ] + } + } + ) + end +end diff --git a/app/models/note_comment.rb b/app/models/note_comment.rb new file mode 100644 index 000000000..bcbcf79be --- /dev/null +++ b/app/models/note_comment.rb @@ -0,0 +1,21 @@ +class NoteComment < ActiveRecord::Base + belongs_to :note, :foreign_key => :note_id + belongs_to :author, :class_name => "User", :foreign_key => :author_id + + validates_presence_of :id, :on => :update + validates_uniqueness_of :id + validates_presence_of :note_id + validates_associated :note + validates_presence_of :visible + validates_associated :author + validates_inclusion_of :event, :in => [ "opened", "closed", "reopened", "commented", "hidden" ] + + # Return the author name + def author_name + if self.author_id.nil? + self.read_attribute(:author_name) + else + self.author.display_name + end + end +end diff --git a/app/models/notifier.rb b/app/models/notifier.rb index e6058d4b7..f025da7b1 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -95,6 +95,22 @@ class Notifier < ActionMailer::Base body :friend => friend end + def note_comment_notification(comment, recipient) + common_headers recipient + owner = (recipient == comment.note.author); + subject I18n.t('notifier.note_plain.subject_own', :commenter => comment.author_name) if owner + subject I18n.t('notifier.note_plain.subject_other', :commenter => comment.author_name) unless owner + + body :nodeurl => url_for(:host => SERVER_URL, + :controller => "browse", + :action => "note", + :id => comment.note_id), + :place => comment.note.nearby_place, + :comment => comment.body, + :owner => owner, + :commenter => comment.author_name + end + private def common_headers(recipient) diff --git a/app/views/browse/_map.html.erb b/app/views/browse/_map.html.erb index 1ff86cd4e..d2695ebe6 100644 --- a/app/views/browse/_map.html.erb +++ b/app/views/browse/_map.html.erb @@ -99,6 +99,15 @@ <% end %> updatelinks(centre.lon, centre.lat, 16, null, minlon, minlat, maxlon, maxlat) + <% elsif map.instance_of? Note %> + var centre = new OpenLayers.LonLat(<%= map.lon %>, <%= map.lat %>); + + setMapCenter(centre, 16); + addMarkerToMap(centre); + + $("loading").style.display = "none"; + + updatelinks(centre.lon, centre.lat, 16, null, null, null, null, null) <% else %> var obj_type = "<%= map.class.name.downcase %>"; var obj_id = <%= map.id %>; diff --git a/app/views/browse/note.html.erb b/app/views/browse/note.html.erb new file mode 100644 index 000000000..5c42fddd6 --- /dev/null +++ b/app/views/browse/note.html.erb @@ -0,0 +1,83 @@ + + + + + + + + + <%= render :partial => "map", :object => @note %> + +
+

+ <% if @note.status == "closed" %> + <%= image_tag("closed_note_marker.png", :alt => 'closed') %> + <%= t 'browse.note.closed_title', :note_name => @note.id %> + <% else %> + <%= image_tag("open_note_marker.png", :alt => 'open') %> + <%= t 'browse.note.open_title', :note_name => @note.id %> + <% end %> +

+
+ <%= render :partial => "navigation" %> +
+ + + + + + + + + + <% if @note.status == "closed" %> + + + + + <% end %> + + + <% if @note.author.nil? %> + + <% else %> + + <% end %> + + + + + + + + + +
<%= t 'browse.note.created_at' %><%= l @note.created_at %>
<%= t 'browse.note.edited_at' %><%= l @note.updated_at %>
<%= t 'browse.note.closed_at' %><%= l @note.closed_at %>
<%= t 'browse.note.opened_by' %> <%= @note.author_name %> <%= link_to h(@note.author_name), :controller => "user", :action => "view", :display_name => @note.author_name %>
<%= t 'browse.note.description' %><%= h(@note.comments.first.body) %>
<%= t 'browse.node_details.coordinates' %>
<%= link_to ("#{number_with_delimiter(@note.lat)}, #{number_with_delimiter(@note.lon)}"), {:controller => 'site', :action => 'index', :lat => h(@note.lat), :lon => h(@note.lon), :zoom => "18"} %>
+ +
+ + <% if @note.comments.length > 1 %> + + + + + + + <% @note.comments[1..-1].each do |comment| %> + + + + + + <% end %> +
<%= t 'browse.note.comment_by' %> <%= t 'browse.note.comment' %> <%= t 'browse.note.date' %>
+ <% if comment.author.nil? %> + <%= comment.author_name %> + <% else %> + <%= link_to h(comment.author.display_name), :controller => "user", :action => "view", :display_name => comment.author.display_name %> + <% end %> + <%= h(comment.body) %> <%= l comment.created_at %>
+ <% end %> + +
+
diff --git a/app/views/note/_note.gpx.builder b/app/views/note/_note.gpx.builder new file mode 100644 index 000000000..91c0cbd98 --- /dev/null +++ b/app/views/note/_note.gpx.builder @@ -0,0 +1,15 @@ +xml.wpt("lon" => note.lon, "lat" => note.lat) do + xml.desc do + xml.cdata! note.flatten_comment("
") + end + + xml.extension do + if note.status = "open" + xml.closed "0" + else + xml.closed "1" + end + + xml.id note.id + end +end diff --git a/app/views/note/_note.rss.builder b/app/views/note/_note.rss.builder new file mode 100644 index 000000000..856b4cc62 --- /dev/null +++ b/app/views/note/_note.rss.builder @@ -0,0 +1,18 @@ +xml.item do + if note.status == "closed" + xml.title t('note.rss.closed', :place => note.nearby_place) + elsif note.comments.length > 1 + xml.title t('note.rss.comment', :place => note.nearby_place) + else + xml.title t('note.rss.new', :place => note.nearby_place) + end + + xml.link url_for(:controller => "browse", :action => "note", :id => note.id, :only_path => false) + xml.guid url_for(:controller => "note", :action => "read", :id => note.id, :only_path => false) + xml.description htmlize(note.flatten_comment("

")) + xml.author note.author_name + xml.pubDate note.updated_at.to_s(:rfc822) + xml.geo :lat, note.lat + xml.geo :long, note.lon + xml.georss :point, "#{note.lat} #{note.lon}" +end diff --git a/app/views/note/_note.xml.builder b/app/views/note/_note.xml.builder new file mode 100644 index 000000000..2a2b2ffc1 --- /dev/null +++ b/app/views/note/_note.xml.builder @@ -0,0 +1,21 @@ +xml.note("lon" => note.lon, "lat" => note.lat) do + xml.id note.id + xml.date_created note.created_at + xml.nearby note.nearby_place + xml.status note.status + + if note.status == "closed" + xml.date_closed note.closed_at + end + + xml.comments do + note.comments.each do |comment| + xml.comment do + xml.date comment.created_at + xml.uid comment.author_id unless comment.author_id.nil? + xml.user comment.author_name + xml.text comment.body + end + end + end +end diff --git a/app/views/note/_notes_paging_nav.html.erb b/app/views/note/_notes_paging_nav.html.erb new file mode 100644 index 000000000..108cbb3d2 --- /dev/null +++ b/app/views/note/_notes_paging_nav.html.erb @@ -0,0 +1,17 @@ +

+ +<% if @page > 1 %> +<%= link_to t('changeset.changeset_paging_nav.previous'), params.merge({ :page => @page - 1 }) %> +<% else %> +<%= t('changeset.changeset_paging_nav.previous') %> +<% end %> + +| <%= t('changeset.changeset_paging_nav.showing_page', :page => @page) %> | + +<% if @notes.size < @page_size %> +<%= t('changeset.changeset_paging_nav.next') %> +<% else %> +<%= link_to t('changeset.changeset_paging_nav.next'), params.merge({ :page => @page + 1 }) %> +<% end %> + +

diff --git a/app/views/note/_user.html.erb b/app/views/note/_user.html.erb new file mode 100644 index 000000000..0e9507650 --- /dev/null +++ b/app/views/note/_user.html.erb @@ -0,0 +1 @@ +<%= link_to user.display_name, :controller => "user", :action => "view", :display_name => user.display_name %> diff --git a/app/views/note/list.gpx.builder b/app/views/note/list.gpx.builder new file mode 100644 index 000000000..7a3046027 --- /dev/null +++ b/app/views/note/list.gpx.builder @@ -0,0 +1,7 @@ +xml.instruct! + +xml.gpx("version" => "1.1", + "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", + "xsi:schemaLocation" => "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd") do + xml << render(:partial => "note", :collection => @notes) +end diff --git a/app/views/note/list.rjs b/app/views/note/list.rjs new file mode 100644 index 000000000..1113c3f08 --- /dev/null +++ b/app/views/note/list.rjs @@ -0,0 +1,6 @@ +@notes.each do |note| + page.call "putAJAXMarker", + note.id, note.lon, note.lat, + note.flatten_comment("
"), + note.status == "open" ? 0 : 1 +end diff --git a/app/views/note/list.rss.builder b/app/views/note/list.rss.builder new file mode 100644 index 000000000..d6ee2bbad --- /dev/null +++ b/app/views/note/list.rss.builder @@ -0,0 +1,13 @@ +xml.instruct! + +xml.rss("version" => "2.0", + "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#", + "xmlns:georss" => "http://www.georss.org/georss") do + xml.channel do + xml.title t('note.rss.title') + xml.description t('note.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon ) + xml.link url_for(:controller => "site", :action => "index", :only_path => false) + + xml << render(:partial => "note", :collection => @notes) + end +end diff --git a/app/views/note/list.xml.builder b/app/views/note/list.xml.builder new file mode 100644 index 000000000..38b239afc --- /dev/null +++ b/app/views/note/list.xml.builder @@ -0,0 +1,3 @@ +xml.instruct! + +xml << render(:partial => "note", :collection => @notes) diff --git a/app/views/note/mine.html.erb b/app/views/note/mine.html.erb new file mode 100644 index 000000000..d81824324 --- /dev/null +++ b/app/views/note/mine.html.erb @@ -0,0 +1,37 @@ +

<%= @heading %>

+

<%= @description %>

+ +<%= render :partial => 'notes_paging_nav' %> + + + + + + + + + + +<% @notes.each do |note| %> + bgcolor="#EEEEEE"<% end %>> + + + <% if note.author.nil? %> + + <% else %> + + <% end %> + + + + +<% end %> +
<%= t'note.mine.id' %><%= t'changeset.changesets.user' %><%= t'changeset.changesets.comment' %><%= t'changeset.changesets.saved_at' %><%= t'note.mine.last_changed' %>
+ <% if note.status == "closed" %> + <%= image_tag("closed_note_marker.png", :alt => 'closed') %> + <% else %> + <%= image_tag("open_note_marker.png", :alt => 'open') %> + <% end %> + <%= link_to note.id.to_s, :controller => "browse", :action => "note", :id => note.id %> <%= note.author_name %> <%= link_to h(note.author_name), :controller => "user", :action => "view", :display_name => note.author_name %> <%= htmlize note.comments.first.body %> <%= l note.created_at %><%= l note.updated_at %>
+ +<%= render :partial => 'notes_paging_nav' %> diff --git a/app/views/note/read.gpx.builder b/app/views/note/read.gpx.builder new file mode 100644 index 000000000..e54d77246 --- /dev/null +++ b/app/views/note/read.gpx.builder @@ -0,0 +1,7 @@ +xml.instruct! + +xml.gpx("version" => "1.1", + "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", + "xsi:schemaLocation" => "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd") do + xml << render(:partial => "note", :object => @note) +end diff --git a/app/views/note/read.rss.builder b/app/views/note/read.rss.builder new file mode 100644 index 000000000..e566ff02e --- /dev/null +++ b/app/views/note/read.rss.builder @@ -0,0 +1,13 @@ +xml.instruct! + +xml.rss("version" => "2.0", + "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#", + "xmlns:georss" => "http://www.georss.org/georss") do + xml.channel do + xml.title t('note.rss.title') + xml.description t('note.rss.description_item', :id => @note.id) + xml.link url_for(:controller => "site", :action => "index", :only_path => false) + + xml << render(:partial => "note", :object => @note) + end +end diff --git a/app/views/note/read.xml.builder b/app/views/note/read.xml.builder new file mode 100644 index 000000000..cfb28c20e --- /dev/null +++ b/app/views/note/read.xml.builder @@ -0,0 +1,3 @@ +xml.instruct! + +xml << render(:partial => "note", :object => @note) diff --git a/app/views/note/rss.rss.builder b/app/views/note/rss.rss.builder new file mode 100644 index 000000000..d22d67311 --- /dev/null +++ b/app/views/note/rss.rss.builder @@ -0,0 +1,46 @@ +xml.instruct! + +xml.rss("version" => "2.0", + "xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#", + "xmlns:georss" => "http://www.georss.org/georss") do + xml.channel do + xml.title t('note.rss.title') + xml.description t('note.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon ) + xml.link url_for(:controller => "site", :action => "index", :only_path => false) + + @comments.each do |comment| + xml.item do + if comment.event == "closed" + xml.title t('note.rss.closed', :place => comment.note.nearby_place) + elsif comment.event == "commented" + xml.title t('note.rss.comment', :place => comment.note.nearby_place) + elsif comment.event == "opened" + xml.title t('note.rss.new', :place => comment.note.nearby_place) + else + xml.title "unknown event" + end + + xml.link url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false) + xml.guid url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false) + + description_text = "" + + if comment.event == "commented" and not comment.nil? + description_text += "Comment:
" + description_text += htmlize(comment.body) + description_text += "
" + end + + description_text += "Full note:
" + description_text += comment.note.flatten_comment("
", comment.created_at) + + xml.description description_text + xml.author comment.author_name + xml.pubDate comment.created_at.to_s(:rfc822) + xml.geo :lat, comment.note.lat + xml.geo :long, comment.note.lon + xml.georss :point, "#{comment.note.lat} #{comment.note.lon}" + end + end + end +end diff --git a/app/views/notifier/note_comment_notification.html.erb b/app/views/notifier/note_comment_notification.html.erb new file mode 100644 index 000000000..fade148d8 --- /dev/null +++ b/app/views/notifier/note_comment_notification.html.erb @@ -0,0 +1,15 @@ +<%= t 'notifier.note_plain.greeting' %> + +<% if @owner %> +<%= t 'notifier.note_plain.your_note', :commenter => @commenter, :place => @place %> +<% else %> +<%= t 'notifier.note_plain.commented_note', :commenter => @commenter, :place => @place %> +<% end %> + +== +<%= @comment %> +== + +<%= t 'notifier.note_plain.details', :URL => @noteurl %> + + diff --git a/app/views/site/index.html.erb b/app/views/site/index.html.erb index c4d990618..340570f6d 100644 --- a/app/views/site/index.html.erb +++ b/app/views/site/index.html.erb @@ -20,6 +20,7 @@ @@ -122,6 +123,7 @@ end <%= javascript_include_tag '/openlayers/OpenLayers.js' %> <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %> +<%= javascript_include_tag 'openstreetbugs.js' %> <%= javascript_include_tag 'map.js' %> <%= render :partial => 'resize' %> @@ -132,6 +134,10 @@ end OpenLayers.Lang.setCode("<%= I18n.locale.to_s %>"); + <% if @user %> + var loginName = "<%= @user.display_name %>" + <% end %> + function mapInit(){ map = createMap("map"); @@ -139,6 +145,25 @@ end map.dataLayer = new OpenLayers.Layer("<%= I18n.t 'browse.start_rjs.data_layer_name' %>", { "visibility": false }); map.dataLayer.events.register("visibilitychanged", map.dataLayer, toggleData); map.addLayer(map.dataLayer); + + map.osbLayer = new OpenLayers.Layer.OpenStreetBugs("Notes", { + serverURL: "/api/0.6/", + iconOpen: new OpenLayers.Icon("<%= image_path "open_note_marker.png" %>", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)), + iconClosed: new OpenLayers.Icon("<%= image_path "closed_noe_marker.png" %>", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)), + readonly: false, + setCookie: false, + permalinkURL: "http://www.openstreetmap.org/", + theme: "<%= stylesheet_path "openstreetbugs" %>", + visibility: false + }); + map.addLayer(map.osbLayer); + + map.osbControl = new OpenLayers.Control.OpenStreetBugs(map.osbLayer); + map.addControl(map.osbControl); + + $("reportbuganchor").observe("click", addBug); + + map.events.register("zoomend", map, allowBugReports); <% end %> <% unless object_zoom %> @@ -267,6 +292,19 @@ end <% end %> } + function addBug() { + map.osbControl.activate(); + map.osbControl.addTemporaryMarker(map.getCenter()); + } + + function allowBugReports() { + if (map.getZoom() > 11) { + $("reportbuganchor").style.visibility = "visible"; + } else { + $("reportbuganchor").style.visibility = "hidden"; + } + } + document.observe("dom:loaded", mapInit); document.observe("dom:loaded", installEditHandler); document.observe("dom:loaded", handleResize); diff --git a/app/views/user/view.html.erb b/app/views/user/view.html.erb index 834e8571b..08c4669ae 100644 --- a/app/views/user/view.html.erb +++ b/app/views/user/view.html.erb @@ -23,7 +23,9 @@ | <%= link_to t('user.view.my edits'), :controller => 'changeset', :action => 'list', :display_name => @user.display_name %> | - <%= link_to t('user.view.my traces'), :controller => 'trace', :action=>'mine' %> + <%= link_to t('user.view.my traces'), :controller => 'trace', :action=> 'mine' %> + | + <%= link_to t('user.view.my notes'), :controller => 'note', :action=> 'mine' %> | <%= link_to t('user.view.my settings'), :controller => 'user', :action => 'account', :display_name => @user.display_name %> | @@ -43,6 +45,8 @@ | <%= link_to t('user.view.traces'), :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %> | + <%= link_to t('user.view.notes'), :controller => 'note', :action=> 'mine' %> + | <% if @user and @user.is_friends_with?(@this_user) %> <%= link_to t('user.view.remove as friend'), :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %> <% else %> diff --git a/config/example.application.yml b/config/example.application.yml index abcd1ce41..cd4f205f3 100644 --- a/config/example.application.yml +++ b/config/example.application.yml @@ -24,6 +24,8 @@ standard_settings: &standard_settings max_number_of_nodes: 50000 # Maximum number of nodes that can be in a way (checked on save) max_number_of_way_nodes: 2000 + # The maximum area you're allowed to request notes from, in square degrees + max_note_request_area: 25 # Zoom level to use for postcode results from the geocoder postcode_zoom: 15 # Zoom level to use for geonames results from the geocoder diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 72aca7e44..18df05cf2 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,5 +1,3 @@ -# Be sure to restart your server when you modify this file. - # Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone + +Mime::Type.register "application/gpx+xml", :gpx diff --git a/config/locales/de.yml b/config/locales/de.yml index 261e46bc5..451555931 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -906,6 +906,22 @@ de: history_disabled_tooltip: Reinzoomen um Änderungen für diesen Bereich anzuzeigen history_tooltip: Änderungen für diesen Bereich anzeigen history_zoom_alert: Du musst näher heranzoomen, um die Chronik zu sehen + osb: + Fixed Error: Behobener Fehler + Unresolved Error: Offener Fehler + Description: Beschreibung + Comment: Kommentar + Has been fixed: Der Fehler wurde bereits behoben. Es kann jedoch bis zu einigen Tagen dauern, bis die Kartenansicht aktualisiert wird. + Comment/Close: Kommentieren/Schließen + Nickname: Benutzername + Add comment: Kommentar hinzufügen + Mark as fixed: Als behoben markieren + Cancel: Abbrechen + Create OpenStreetBug: OpenStreetBug melden + Create bug: Bug anlegen + Bug description: Fehlerbeschreibung + Create: Anlegeeen + Permalink: Permalink layouts: community_blogs: Blogs community_blogs_title: Blogs von Mitwirkenden bei OpenStreetMap diff --git a/config/locales/en.yml b/config/locales/en.yml index fd9c49b2b..598301c2a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -282,6 +282,17 @@ en: download_xml: "Download XML" view_history: "view history" edit: "edit" + note: + open_title: "Unresolved issue: %{note_name}" + closed_title: "Resolved issue: %{note_name}" + created_at: "Created at:" + edited_at: "Edited at:" + closed_at: "Closed at:" + opened_by: "Opened by:" + description: "Description:" + comment_by: "Comment by: " + comment: "Comment:" + date: "Date:" changeset: changeset_paging_nav: showing_page: "Showing page %{page}" @@ -1176,6 +1187,13 @@ en: greeting: "Hi," hopefully_you: "Someone (possibly you) has asked for the password to be reset on this email address's openstreetmap.org account." click_the_link: "If this is you, please click the link below to reset your password." + note_plain: + subject_own: "[OpenStreetMap] %{commenter} has commented on one of your notes" + subject_other: "[OpenStreetMap] %{commenter} has commented on a note you are interested in" + greeting: "Hi," + your_note: "%{commenter} has left a comment on one of your map notes near %{place}." + commented_note: "%{commenter} has left a comment on a map note you have commented on. The note is near %{place}." + details: "More details about the note can be found at %{URL}." message: inbox: title: "Inbox" @@ -1642,6 +1660,7 @@ en: new diary entry: new diary entry my edits: my edits my traces: my traces + my notes: my map notes my settings: my settings oauth settings: oauth settings blocks on me: blocks on me @@ -1650,6 +1669,7 @@ en: diary: diary edits: edits traces: traces + notes: map notes remove as friend: remove as friend add as friend: add as friend mapper since: "Mapper since:" @@ -1901,6 +1921,21 @@ en: back: "View all blocks" revoker: "Revoker:" needs_view: "The user needs to log in before this block will be cleared." + note: + rss: + title: "OpenStreetMap Notes" + description_area: "A list of notes, reported, commented on or closed in your area [(%{min_lat}|%{min_lon}) -- (%{max_lat}|%{max_lon})]" + description_item: "An rss feed for note %{id}" + closed: "closed note (near %{place})" + new: "new note (near %{place})" + comment: "new comment (near %{place})" + mine: + title: "Notes submitted or commented on by %{user}" + heading: "%{user}'s notes" + description: "Notes submitted or commented on by %{user}" + id: "Id" + last_changed: "Last changed" + javascripts: map: base: @@ -1917,3 +1952,25 @@ en: history_tooltip: View edits for this area history_disabled_tooltip: Zoom in to view edits for this area history_zoom_alert: You must zoom in to view edits for this area + osb: + Fixed Error: Fixed Error + Unresolved Error: Unresolved Error + Description: Description + Comment: Comment + Has been fixed: This error has been fixed already. However, it might take a couple of days before the map image is updated. + Comment/Close: Comment/Close + Nickname: Nickname + Add comment: Add comment + Mark as fixed: Mark as fixed + Cancel: Cancel + Create OpenStreetBug: Create OpenStreetBug + Create bug: Report a problem with the map + Bug description: Problem description + Create: Report problem + Permalink: Permalink + Login: Login + Details: Details + edityourself: You can also edit the map directly your self + draghelp1: Please drag the marker to the location of the problem + draghelp2: and descripe it as accurate as possible + diff --git a/config/routes.rb b/config/routes.rb index 56a59a207..334cb7422 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,23 @@ ActionController::Routing::Routes.draw do |map| map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read' map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write' map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints' - + + # Map notes API + map.connect "api/#{API_VERSION}/notes", :controller => 'note', :action => 'list' + map.connect "api/#{API_VERSION}/notes/search", :controller => 'note', :action => 'search' + map.connect "api/#{API_VERSION}/notes/rss", :controller =>'notes', :action => 'rss' + map.connect "api/#{API_VERSION}/note/create", :controller => 'note', :action => 'create' + map.connect "api/#{API_VERSION}/note/:id/comment", :controller => 'note', :action => 'update', :id => /\d+/ + map.connect "api/#{API_VERSION}/note/:id/close", :controller => 'note', :action => 'close', :id => /\d+/ + map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'read', :id => /\d+/, :conditions => { :method => :get } + map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete } + map.connect "api/#{API_VERSION}/notes/getBugs", :controller => 'note', :action => 'list' + map.connect "api/#{API_VERSION}/notes/addPOIexec", :controller => 'note', :action => 'create' + map.connect "api/#{API_VERSION}/notes/closePOIexec", :controller => 'note', :action => 'close' + map.connect "api/#{API_VERSION}/notes/editPOIexec", :controller => 'note', :action => 'update' + map.connect "api/#{API_VERSION}/notes/getGPX", :controller => 'note', :action => 'list', :format => :gpx + map.connect "api/#{API_VERSION}/notes/getRSSfeed", :controller => 'note', :action => 'rss' + # Data browsing map.connect '/browse/start', :controller => 'browse', :action => 'start' map.connect '/browse/way/:id', :controller => 'browse', :action => 'way', :id => /\d+/ @@ -88,6 +104,8 @@ ActionController::Routing::Routes.draw do |map| map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list' map.connect '/browse/changesets/feed', :controller => 'changeset', :action => 'list', :format => :atom map.connect '/browse/changesets', :controller => 'changeset', :action => 'list' + map.connect '/browse/note/:id', :controller => 'browse', :action => 'note', :id => /\d+/ + map.connect '/user/:display_name/notes', :controller => 'note', :action => 'mine' map.connect '/browse', :controller => 'changeset', :action => 'list' # web site diff --git a/db/migrate/053_add_map_bug_tables.rb b/db/migrate/053_add_map_bug_tables.rb new file mode 100644 index 000000000..8d444a49f --- /dev/null +++ b/db/migrate/053_add_map_bug_tables.rb @@ -0,0 +1,33 @@ +require 'lib/migrate' + +class AddMapBugTables < ActiveRecord::Migration + def self.up + create_enumeration :map_bug_status_enum, ["open", "closed", "hidden"] + + create_table :map_bugs do |t| + t.column :id, :bigint, :null => false + t.integer :latitude, :null => false + t.integer :longitude, :null => false + t.column :tile, :bigint, :null => false + t.datetime :last_changed, :null => false + t.datetime :date_created, :null => false + t.string :nearby_place + t.string :text + t.column :status, :map_bug_status_enum, :null => false + end + + add_index :map_bugs, [:tile, :status], :name => "map_bugs_tile_idx" + add_index :map_bugs, [:last_changed], :name => "map_bugs_changed_idx" + add_index :map_bugs, [:date_created], :name => "map_bugs_created_idx" + end + + def self.down + remove_index :map_bugs, :name => "map_bugs_tile_idx" + remove_index :map_bugs, :name => "map_bugs_changed_idx" + remove_index :map_bugs, :name => "map_bugs_created_idx" + + drop_table :map_bugs + + drop_enumeration :map_bug_status_enum + end +end diff --git a/db/migrate/054_refactor_map_bug_tables.rb b/db/migrate/054_refactor_map_bug_tables.rb new file mode 100644 index 000000000..6d259d20a --- /dev/null +++ b/db/migrate/054_refactor_map_bug_tables.rb @@ -0,0 +1,34 @@ +require 'lib/migrate' + +class RefactorMapBugTables < ActiveRecord::Migration + def self.up + create_table :map_bug_comment do |t| + t.column :id, :bigint, :null => false + t.column :bug_id, :bigint, :null => false + t.boolean :visible, :null => false + t.datetime :date_created, :null => false + t.string :commenter_name + t.string :commenter_ip + t.column :commenter_id, :bigint + t.string :comment + end + + remove_column :map_bugs, :text + + add_index :map_bug_comment, [:bug_id], :name => "map_bug_comment_id_idx" + + add_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id] + add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id] + end + + def self.down + remove_foreign_key :map_bug_comment, [:commenter_id] + remove_foreign_key :map_bug_comment, [:bug_id] + + remove_index :map_bugs, :name => "map_bug_comment_id_idx" + + add_column :map_bugs, :text, :string + + drop_table :map_bug_comment + end +end diff --git a/db/migrate/055_change_map_bug_comment_type.rb b/db/migrate/055_change_map_bug_comment_type.rb new file mode 100644 index 000000000..2a64bf216 --- /dev/null +++ b/db/migrate/055_change_map_bug_comment_type.rb @@ -0,0 +1,11 @@ +require 'lib/migrate' + +class ChangeMapBugCommentType < ActiveRecord::Migration + def self.up + change_column :map_bug_comment, :comment, :text + end + + def self.down + change_column :map_bug_comment, :comment, :string + end +end diff --git a/db/migrate/056_add_date_closed.rb b/db/migrate/056_add_date_closed.rb new file mode 100644 index 000000000..c5aa2c2f5 --- /dev/null +++ b/db/migrate/056_add_date_closed.rb @@ -0,0 +1,11 @@ +require 'lib/migrate' + +class AddDateClosed < ActiveRecord::Migration + def self.up + add_column :map_bugs, :date_closed, :timestamp + end + + def self.down + remove_column :map_bugs, :date_closed + end +end diff --git a/db/migrate/057_add_map_bug_comment_event.rb b/db/migrate/057_add_map_bug_comment_event.rb new file mode 100644 index 000000000..c13c1f9d5 --- /dev/null +++ b/db/migrate/057_add_map_bug_comment_event.rb @@ -0,0 +1,15 @@ +require 'lib/migrate' + +class AddMapBugCommentEvent < ActiveRecord::Migration + def self.up + create_enumeration :map_bug_event_enum, ["opened", "closed", "reopened", "commented", "hidden"] + + add_column :map_bug_comment, :event, :map_bug_event_enum + end + + def self.down + remove_column :map_bug_comment, :event + + drop_enumeration :map_bug_event_enum + end +end diff --git a/db/migrate/20110508145337_cleanup_bug_tables.rb b/db/migrate/20110508145337_cleanup_bug_tables.rb new file mode 100644 index 000000000..e7dfcb793 --- /dev/null +++ b/db/migrate/20110508145337_cleanup_bug_tables.rb @@ -0,0 +1,25 @@ +class CleanupBugTables < ActiveRecord::Migration + def self.up + rename_column :map_bugs, :date_created, :created_at + rename_column :map_bugs, :last_changed, :updated_at + rename_column :map_bugs, :date_closed, :closed_at + + rename_column :map_bug_comment, :date_created, :created_at + rename_column :map_bug_comment, :commenter_name, :author_name + rename_column :map_bug_comment, :commenter_ip, :author_ip + rename_column :map_bug_comment, :commenter_id, :author_id + rename_column :map_bug_comment, :comment, :body + end + + def self.down + rename_column :map_bug_comment, :body, :comment + rename_column :map_bug_comment, :author_id, :commenter_id + rename_column :map_bug_comment, :author_ip, :commenter_ip + rename_column :map_bug_comment, :author_name, :commenter_name + rename_column :map_bug_comment, :created_at, :date_created + + rename_column :map_bugs, :closed_at, :date_closed + rename_column :map_bugs, :updated_at, :last_changed + rename_column :map_bugs, :created_at, :date_created + end +end diff --git a/db/migrate/20110521142405_rename_bugs_to_notes.rb b/db/migrate/20110521142405_rename_bugs_to_notes.rb new file mode 100644 index 000000000..240d4474f --- /dev/null +++ b/db/migrate/20110521142405_rename_bugs_to_notes.rb @@ -0,0 +1,55 @@ +require 'lib/migrate' + +class RenameBugsToNotes < ActiveRecord::Migration + def self.up + rename_enumeration "map_bug_status_enum", "note_status_enum" + rename_enumeration "map_bug_event_enum", "note_event_enum" + + rename_table :map_bugs, :notes + rename_sequence :notes, "map_bugs_id_seq", "notes_id_seq" + rename_index :notes, "map_bugs_pkey", "notes_pkey" + rename_index :notes, "map_bugs_changed_idx", "notes_updated_at_idx" + rename_index :notes, "map_bugs_created_idx", "notes_created_at_idx" + rename_index :notes, "map_bugs_tile_idx", "notes_tile_status_idx" + + remove_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id] + rename_column :map_bug_comment, :author_id, :commenter_id + remove_foreign_key :map_bug_comment, [:commenter_id], :users, [:id] + rename_column :map_bug_comment, :commenter_id, :author_id + + rename_table :map_bug_comment, :note_comments + rename_column :note_comments, :bug_id, :note_id + rename_sequence :note_comments, "map_bug_comment_id_seq", "note_comments_id_seq" + rename_index :note_comments, "map_bug_comment_pkey", "note_comments_pkey" + rename_index :note_comments, "map_bug_comment_id_idx", "note_comments_note_id_idx" + + add_foreign_key :note_comments, [:note_id], :notes, [:id] + add_foreign_key :note_comments, [:author_id], :users, [:id] + end + + def self.down + remove_foreign_key :note_comments, [:author_id], :users, [:id] + remove_foreign_key :note_comments, [:note_id], :notes, [:id] + + rename_index :note_comments, "note_comments_note_id_idx", "map_bug_comment_id_idx" + rename_index :notes, "note_comments_pkey", "map_bug_comment_pkey" + rename_column :note_comments, :note_id, :bug_id + rename_sequence :note_comments, "note_comments_id_seq", "map_bug_comment_id_seq" + rename_table :note_comments, :map_bug_comment + + rename_column :map_bug_comment, :author_id, :commenter_id + add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id] + rename_column :map_bug_comment, :commenter_id, :author_id + add_foreign_key :map_bug_comment, [:bug_id], :notes, [:id] + + rename_index :notes, "notes_tile_status_idx", "map_bugs_tile_idx" + rename_index :notes, "notes_created_at_idx", "map_bugs_created_idx" + rename_index :notes, "notes_updated_at_idx", "map_bugs_changed_idx" + rename_index :notes, "notes_pkey", "map_bugs_pkey" + rename_sequence :notes, "notes_id_seq", "map_bugs_id_seq" + rename_table :notes, :map_bugs + + rename_enumeration "note_event_enum", "map_bug_event_enum" + rename_enumeration "note_status_enum", "map_bug_status_enum" + end +end diff --git a/lib/geo_record.rb b/lib/geo_record.rb index 2740eab0c..90dee5f1d 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -56,4 +56,3 @@ private end end end - diff --git a/lib/map_boundary.rb b/lib/map_boundary.rb index f3accf2da..b3085d0ec 100644 --- a/lib/map_boundary.rb +++ b/lib/map_boundary.rb @@ -9,7 +9,7 @@ module MapBoundary return min_lon, min_lat, max_lon, max_lat end - def check_boundaries(min_lon, min_lat, max_lon, max_lat) + def check_boundaries(min_lon, min_lat, max_lon, max_lat, max_area = MAX_REQUEST_AREA) # check the bbox is sane unless min_lon <= max_lon raise OSM::APIBadBoundingBox.new("The minimum longitude must be less than the maximum longitude, but it wasn't") @@ -24,8 +24,8 @@ module MapBoundary # check the bbox isn't too large requested_area = (max_lat-min_lat)*(max_lon-min_lon) - if requested_area > MAX_REQUEST_AREA - raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + MAX_REQUEST_AREA.to_s + + if requested_area > max_area + raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + max_area.to_s + ", and your request was too large. Either request a smaller area, or use planet.osm") end end diff --git a/lib/migrate.rb b/lib/migrate.rb index 81cdd4d05..8e6629f0d 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -105,11 +105,11 @@ module ActiveRecord @enumerations ||= Hash.new end - def create_enumeration (enumeration_name, values) + def create_enumeration(enumeration_name, values) enumerations[enumeration_name] = values end - def drop_enumeration (enumeration_name) + def drop_enumeration(enumeration_name) enumerations.delete(enumeration_name) end @@ -158,29 +158,34 @@ module ActiveRecord return "" end - def change_engine (table_name, engine) + def change_engine(table_name, engine) end - def add_fulltext_index (table_name, column) - execute "CREATE INDEX #{table_name}_#{column}_idx on #{table_name} (#{column})" + def add_fulltext_index(table_name, column) + execute "CREATE INDEX #{table_name}_#{column}_idx ON #{table_name} (#{column})" end def enumerations @enumerations ||= Hash.new end - def create_enumeration (enumeration_name, values) + def create_enumeration(enumeration_name, values) enumerations[enumeration_name] = values - execute "create type #{enumeration_name} as enum ('#{values.join '\',\''}')" + execute "CREATE TYPE #{enumeration_name} AS ENUM ('#{values.join '\',\''}')" end - def drop_enumeration (enumeration_name) - execute "drop type #{enumeration_name}" + def drop_enumeration(enumeration_name) + execute "DROP TYPE #{enumeration_name}" enumerations.delete(enumeration_name) end + def rename_enumeration(old_name, new_name) + execute "ALTER TYPE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + def alter_primary_key(table_name, new_columns) - execute "alter table #{table_name} drop constraint #{table_name}_pkey; alter table #{table_name} add primary key (#{new_columns.join(',')})" + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" + execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (#{new_columns.join(',')})" end def interval_constant(interval) @@ -201,6 +206,14 @@ module ActiveRecord quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ") execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} USING #{index_method} (#{quoted_column_names})" end + + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + + def rename_sequence(table_name, old_name, new_name) + execute "ALTER SEQUENCE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end end end end diff --git a/lib/osm.rb b/lib/osm.rb index eaee7c328..9ee36e0c7 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -499,6 +499,7 @@ module OSM # Return an SQL fragment to select a given area of the globe def self.sql_for_area(minlat, minlon, maxlat, maxlon, prefix = nil) tilesql = QuadTile.sql_for_area(minlat, minlon, maxlat, maxlon, prefix) + minlat = (minlat * 10000000).round minlon = (minlon * 10000000).round maxlat = (maxlat * 10000000).round diff --git a/public/images/closed_note_marker.png b/public/images/closed_note_marker.png new file mode 100644 index 000000000..bf6d6bb25 Binary files /dev/null and b/public/images/closed_note_marker.png differ diff --git a/public/images/icon_error_add.png b/public/images/icon_error_add.png new file mode 100644 index 000000000..671cf424c Binary files /dev/null and b/public/images/icon_error_add.png differ diff --git a/public/images/open_note_marker.png b/public/images/open_note_marker.png new file mode 100644 index 000000000..a58031663 Binary files /dev/null and b/public/images/open_note_marker.png differ diff --git a/public/javascripts/openstreetbugs.js b/public/javascripts/openstreetbugs.js new file mode 100644 index 000000000..12bd7d859 --- /dev/null +++ b/public/javascripts/openstreetbugs.js @@ -0,0 +1,989 @@ +/* + This OpenStreetBugs client is free software: you can redistribute it + and/or modify it under the terms of the GNU Affero General Public License + as published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This file is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + License for more details. +*/ + +/** + * A fully functional OpenStreetBugs layer. See http://openstreetbugs.schokokeks.org/. + * Even though the OpenStreetBugs API originally does not intend this, you can create multiple instances of this Layer and add them to different maps (or to one single map for whatever crazy reason) without problems. +*/ + +/** This version has been adapted from the original javascript library to fit the openstreetmap rails_port implementation */ + +OpenLayers.Layer.OpenStreetBugs = new OpenLayers.Class(OpenLayers.Layer.Markers, { + /** + * The URL of the OpenStreetBugs API. + * @var String + */ + serverURL : "/api/0.6/", + + /** + * Associative array (index: bug ID) that is filled with the bugs loaded in this layer + * @var String + */ + bugs : { }, + + /** + * The username to be used to change or create bugs on OpenStreetBugs + * @var String + */ + username : "NoName", + + /** + * The icon to be used for an open bug + * @var OpenLayers.Icon + */ + iconOpen : new OpenLayers.Icon("/images/open_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)), + + /** + * The icon to be used for a closed bug + * @var OpenLayers.Icon + */ + iconClosed : new OpenLayers.Icon("/images/closed_bug_marker.png", new OpenLayers.Size(22, 22), new OpenLayers.Pixel(-11, -11)), + + /** + * The projection of the coordinates sent by the OpenStreetBugs API. + * @var OpenLayers.Projection + */ + apiProjection : new OpenLayers.Projection("EPSG:4326"), + + /** + * If this is set to true, the user may not commit comments or close bugs. + * @var Boolean + */ + readonly : false, + + /** + * When the layer is hidden, all open popups are stored in this array in order to be re-opened again when the layer is made visible again. + */ + reopenPopups : [ ], + + /** + * The user name will be saved in a cookie if this isn’t set to false. + * @var Boolean + */ + setCookie : true, + + /** + * The lifetime of the user name cookie in days. + * @var Number + */ + cookieLifetime : 1000, + + /** + * The path where the cookie will be available on this server. + * @var String + */ + cookiePath : null, + + /** + * A URL to append lon=123&lat=123&zoom=123 for the Permalinks. + * @var String + */ + permalinkURL : "http://www.openstreetmap.org/", + + /** + * A CSS file to be included. Set to null if you don’t need this. + * @var String + */ + theme : "http://osm.cdauth.de/map/openstreetbugs.css", + + /** + * @param String name + */ + initialize : function(name, options) + { + OpenLayers.Layer.Markers.prototype.initialize.apply(this, [ name, OpenLayers.Util.extend({ opacity: 0.7, projection: new OpenLayers.Projection("EPSG:4326") }, options) ]); + putAJAXMarker.layers.push(this); + this.events.addEventType("markerAdded"); + + this.events.register("visibilitychanged", this, this.updatePopupVisibility); + this.events.register("visibilitychanged", this, this.loadBugs); + + var cookies = document.cookie.split(/;\s*/); + for(var i=0; i/); + for(var i=0; i").replace(/&/g, "&"); + putAJAXMarker.bugs[id] = [ + new OpenLayers.LonLat(lon, lat), + comments, + closed + ]; + for(var i=0; i + +open_note: + id: 1 + latitude: <%= 1*SCALE %> + longitude: <%= 1*SCALE %> + status: open + tile: <%= QuadTile.tile_for_point(1,1) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-01-01 00:00:00 + +open_note_with_comment: + id: 2 + latitude: <%= 1.1*SCALE %> + longitude: <%= 1.1*SCALE %> + status: open + tile: <%= QuadTile.tile_for_point(1.1,1.1) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-02-01 00:00:00 + +closed_note_with_comment: + id: 3 + latitude: <%= 1.2*SCALE %> + longitude: <%= 1.2*SCALE %> + status: closed + tile: <%= QuadTile.tile_for_point(1.2,1.2) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-03-01 00:00:00 + closed_at: 2007-03-01 00:00:00 + +hidden_note_with_comment: + id: 4 + latitude: <%= 1.3*SCALE %> + longitude: <%= 1.3*SCALE %> + status: hidden + tile: <%= QuadTile.tile_for_point(1.3,1.3) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-03-01 00:00:00 + +note_with_hidden_comment: + id: 5 + latitude: <%= 1.4*SCALE %> + longitude: <%= 1.4*SCALE %> + status: open + tile: <%= QuadTile.tile_for_point(1.4,1.4) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-03-01 00:00:00 + +note_with_comments_by_users: + id: 6 + latitude: <%= 1.5*SCALE %> + longitude: <%= 1.5*SCALE %> + status: open + tile: <%= QuadTile.tile_for_point(1.5,1.5) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-03-01 00:00:00 + +note_closed_by_user: + id: 7 + latitude: <%= 1.6*SCALE %> + longitude: <%= 1.6*SCALE %> + status: closed + tile: <%= QuadTile.tile_for_point(1.6,1.6) %> + created_at: 2007-01-01 00:00:00 + updated_at: 2007-03-01 00:00:00 + closed_at: 2007-03-01 00:00:00 diff --git a/test/functional/note_controller_test.rb b/test/functional/note_controller_test.rb new file mode 100644 index 000000000..2e4a01b3e --- /dev/null +++ b/test/functional/note_controller_test.rb @@ -0,0 +1,324 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class NoteControllerTest < ActionController::TestCase + fixtures :users, :notes, :note_comments + + def test_note_create_success + assert_difference('Note.count') do + assert_difference('NoteComment.count') do + post :create, {:lat => -1.0, :lon => -1.0, :name => "new_tester", :text => "This is a comment"} + end + end + assert_response :success + id = @response.body.sub(/ok/,"").to_i + + get :read, {:id => id, :format => 'json'} + assert_response :success + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal id, js["note"]["id"] + assert_equal "open", js["note"]["status"] + assert_equal "opened", js["note"]["comments"].last["event"] + assert_equal "This is a comment", js["note"]["comments"].last["body"] + assert_equal "new_tester (a)", js["note"]["comments"].last["author_name"] + end + + def test_note_create_fail + assert_no_difference('Note.count') do + assert_no_difference('NoteComment.count') do + post :create, {:lon => -1.0, :name => "new_tester", :text => "This is a comment"} + end + end + assert_response :bad_request + + assert_no_difference('Note.count') do + assert_no_difference('NoteComment.count') do + post :create, {:lat => -1.0, :name => "new_tester", :text => "This is a comment"} + end + end + assert_response :bad_request + + assert_no_difference('Note.count') do + assert_no_difference('NoteComment.count') do + post :create, {:lat => -1.0, :lon => -1.0, :name => "new_tester"} + end + end + assert_response :bad_request + + assert_no_difference('Note.count') do + assert_no_difference('NoteComment.count') do + post :create, {:lat => -100.0, :lon => -1.0, :name => "new_tester", :text => "This is a comment"} + end + end + assert_response :bad_request + + assert_no_difference('Note.count') do + assert_no_difference('NoteComment.count') do + post :create, {:lat => -1.0, :lon => -200.0, :name => "new_tester", :text => "This is a comment"} + end + end + assert_response :bad_request + end + + def test_note_comment_create_success + assert_difference('NoteComment.count') do + post :update, {:id => notes(:open_note_with_comment).id, :name => "new_tester2", :text => "This is an additional comment"} + end + assert_response :success + + get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'} + assert_response :success + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal notes(:open_note_with_comment).id, js["note"]["id"] + assert_equal "open", js["note"]["status"] + assert_equal "commented", js["note"]["comments"].last["event"] + assert_equal "This is an additional comment", js["note"]["comments"].last["body"] + assert_equal "new_tester2 (a)", js["note"]["comments"].last["author_name"] + end + + def test_note_comment_create_fail + assert_no_difference('NoteComment.count') do + post :update, {:name => "new_tester2", :text => "This is an additional comment"} + end + assert_response :bad_request + + assert_no_difference('NoteComment.count') do + post :update, {:id => notes(:open_note_with_comment).id, :name => "new_tester2"} + end + assert_response :bad_request + + assert_no_difference('NoteComment.count') do + post :update, {:id => 12345, :name => "new_tester2", :text => "This is an additional comment"} + end + assert_response :not_found + + assert_no_difference('NoteComment.count') do + post :update, {:id => notes(:hidden_note_with_comment).id, :name => "new_tester2", :text => "This is an additional comment"} + end + assert_response :gone + end + + def test_note_close_success + post :close, {:id => notes(:open_note_with_comment).id} + assert_response :success + + get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'} + assert_response :success + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal notes(:open_note_with_comment).id, js["note"]["id"] + assert_equal "closed", js["note"]["status"] + assert_equal "closed", js["note"]["comments"].last["event"] + assert_equal "NoName (a)", js["note"]["comments"].last["author_name"] + end + + def test_note_close_fail + post :close + assert_response :bad_request + + post :close, {:id => 12345} + assert_response :not_found + + post :close, {:id => notes(:hidden_note_with_comment).id} + assert_response :gone + end + + def test_note_read_success + get :read, {:id => notes(:open_note).id} + assert_response :success + assert_equal "application/xml", @response.content_type + + get :read, {:id => notes(:open_note).id, :format => "xml"} + assert_response :success + assert_equal "application/xml", @response.content_type + + get :read, {:id => notes(:open_note).id, :format => "rss"} + assert_response :success + assert_equal "application/rss+xml", @response.content_type + + get :read, {:id => notes(:open_note).id, :format => "json"} + assert_response :success + assert_equal "application/json", @response.content_type + + get :read, {:id => notes(:open_note).id, :format => "gpx"} + assert_response :success + assert_equal "application/gpx+xml", @response.content_type + end + + def test_note_read_hidden_comment + get :read, {:id => notes(:note_with_hidden_comment).id, :format => 'json'} + assert_response :success + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal notes(:note_with_hidden_comment).id, js["note"]["id"] + assert_equal 2, js["note"]["comments"].count + assert_equal "Valid comment for note 5", js["note"]["comments"][0]["body"] + assert_equal "Another valid comment for note 5", js["note"]["comments"][1]["body"] + end + + def test_note_read_fail + post :read + assert_response :bad_request + + get :read, {:id => 12345} + assert_response :not_found + + get :read, {:id => notes(:hidden_note_with_comment).id} + assert_response :gone + end + + def test_note_delete_success + delete :delete, {:id => notes(:open_note_with_comment).id} + assert_response :success + + get :read, {:id => notes(:open_note_with_comment).id, :format => 'json'} + assert_response :gone + end + + def test_note_delete_fail + delete :delete + assert_response :bad_request + + delete :delete, {:id => 12345} + assert_response :not_found + + delete :delete, {:id => notes(:hidden_note_with_comment).id} + assert_response :gone + end + + def test_get_notes_success + get :list, {:bbox => '1,1,1.2,1.2'} + assert_response :success + assert_equal "text/javascript", @response.content_type + + get :list, {:bbox => '1,1,1.2,1.2', :format => 'rss'} + assert_response :success + assert_equal "application/rss+xml", @response.content_type + + get :list, {:bbox => '1,1,1.2,1.2', :format => 'json'} + assert_response :success + assert_equal "application/json", @response.content_type + + get :list, {:bbox => '1,1,1.2,1.2', :format => 'xml'} + assert_response :success + assert_equal "application/xml", @response.content_type + + get :list, {:bbox => '1,1,1.2,1.2', :format => 'gpx'} + assert_response :success + assert_equal "application/gpx+xml", @response.content_type + end + + def test_get_notes_large_area + get :list, {:bbox => '-2.5,-2.5,2.5,2.5'} + assert_response :success + + get :list, {:l => '-2.5', :b => '-2.5', :r => '2.5', :t => '2.5'} + assert_response :success + + get :list, {:bbox => '-10,-10,12,12'} + assert_response :bad_request + + get :list, {:l => '-10', :b => '-10', :r => '12', :t => '12'} + assert_response :bad_request + end + + def test_get_notes_closed + get :list, {:bbox=>'1,1,1.7,1.7', :closed => '7', :format => 'json'} + assert_response :success + assert_equal "application/json", @response.content_type + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal 4, js.count + + get :list, {:bbox=>'1,1,1.7,1.7', :closed => '0', :format => 'json'} + assert_response :success + assert_equal "application/json", @response.content_type + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal 4, js.count + + get :list, {:bbox=>'1,1,1.7,1.7', :closed => '-1', :format => 'json'} + assert_response :success + assert_equal "application/json", @response.content_type + js = ActiveSupport::JSON.decode(@response.body) + assert_not_nil js + assert_equal 6, js.count + end + + def test_get_notes_bad_params + get :list, {:bbox => '-2.5,-2.5,2.5'} + assert_response :bad_request + + get :list, {:bbox => '-2.5,-2.5,2.5,2.5,2.5'} + assert_response :bad_request + + get :list, {:b => '-2.5', :r => '2.5', :t => '2.5'} + assert_response :bad_request + + get :list, {:l => '-2.5', :r => '2.5', :t => '2.5'} + assert_response :bad_request + + get :list, {:l => '-2.5', :b => '-2.5', :t => '2.5'} + assert_response :bad_request + + get :list, {:l => '-2.5', :b => '-2.5', :r => '2.5'} + assert_response :bad_request + end + + def test_search_success + get :search, {:q => 'note 1'} + assert_response :success + assert_equal "text/javascript", @response.content_type + + get :search, {:q => 'note 1', :format => 'xml'} + assert_response :success + assert_equal "application/xml", @response.content_type + + get :search, {:q => 'note 1', :format => 'json'} + assert_response :success + assert_equal "application/json", @response.content_type + + get :search, {:q => 'note 1', :format => 'rss'} + assert_response :success + assert_equal "application/rss+xml", @response.content_type + + get :search, {:q => 'note 1', :format => 'gpx'} + assert_response :success + assert_equal "application/gpx+xml", @response.content_type + end + + def test_search_bad_params + get :search + assert_response :bad_request + end + + def test_rss_success + get :rss + assert_response :success + assert_equal "application/rss+xml", @response.content_type + + get :rss, {:bbox=>'1,1,1.2,1.2'} + assert_response :success + assert_equal "application/rss+xml", @response.content_type + end + + def test_rss_fail + get :rss, {:bbox=>'1,1,1.2'} + assert_response :bad_request + + get :rss, {:bbox=>'1,1,1.2,1.2,1.2'} + assert_response :bad_request + end + + def test_user_notes_success + get :mine, {:display_name=>'test'} + assert_response :success + + get :mine, {:display_name=>'pulibc_test2'} + assert_response :success + + get :mine, {:display_name=>'non-existent'} + assert_response :not_found + end +end