From: Tom Hughes Date: Wed, 22 Aug 2012 19:52:08 +0000 (+0100) Subject: Merge branch 'master' into openstreetbugs X-Git-Tag: live~6110^2~106 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/0d3a9ed9cb47ce3b89ea9eaffbb589f9a9ff6d22?hp=a9a5b6ef38aeddcae590dfc53763ac303cadb8b0 Merge branch 'master' into openstreetbugs Conflicts: Gemfile.lock app/views/browse/_map.html.erb app/views/user/view.html.erb config/locales/en.yml config/openlayers.cfg db/structure.sql vendor/assets/openlayers/OpenLayers.js --- diff --git a/Gemfile b/Gemfile index f4be08961..25b10fe7b 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ gem 'composite_primary_keys', '>= 5.0.8' gem 'http_accept_language', '>= 1.0.2' gem 'paperclip', '~> 2.0' gem 'deadlock_retry', '>= 1.2.0' +gem 'jsonify-rails' # We need ruby-openid 2.2.0 or later for ruby 1.9 support gem 'ruby-openid', '>= 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 6f2e73d43..158694564 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,10 +58,15 @@ GEM i18n (0.6.0) iconv (0.1) journey (1.0.4) - jquery-rails (2.1.0) + jquery-rails (2.1.1) railties (>= 3.1.0, < 5.0) thor (~> 0.14) json (1.7.5) + jsonify (0.3.1) + multi_json (~> 1.0) + jsonify-rails (0.3.2) + actionpack + jsonify (< 0.4.0) jwt (0.1.5) multi_json (>= 1.0) libv8 (3.3.10.4) @@ -171,6 +176,7 @@ DEPENDENCIES httpclient iconv jquery-rails + jsonify-rails libxml-ruby (>= 2.0.5) memcached (>= 1.4.1) minitest diff --git a/app/assets/images/closed_note_marker.png b/app/assets/images/closed_note_marker.png new file mode 100644 index 000000000..bf6d6bb25 Binary files /dev/null and b/app/assets/images/closed_note_marker.png differ diff --git a/app/assets/images/new_note_marker.png b/app/assets/images/new_note_marker.png new file mode 100644 index 000000000..671cf424c Binary files /dev/null and b/app/assets/images/new_note_marker.png differ diff --git a/app/assets/images/open_note_marker.png b/app/assets/images/open_note_marker.png new file mode 100644 index 000000000..a58031663 Binary files /dev/null and b/app/assets/images/open_note_marker.png differ diff --git a/app/assets/javascripts/notes.js.erb b/app/assets/javascripts/notes.js.erb new file mode 100644 index 000000000..36eabeaee --- /dev/null +++ b/app/assets/javascripts/notes.js.erb @@ -0,0 +1,156 @@ +function addNoteLayer(map, notesUrl, newNoteControls, minZoom) { + var newNotes; + + var noteCallback = function (scope, response) { + for (var f = 0; f < response.features.length; f++) { + var feature = response.features[f]; + } + }; + + var saveNewNotes = function (o) { + var layer = o.object; + newNotes = layer.getFeaturesByAttribute("status", "new") + layer.removeFeatures(newNotes, { silent: true }); + }; + + var restoreNewNotes = function (o) { + var layer = o.object; + layer.addFeatures(newNotes); + newNotes = undefined; + }; + + var noteSelected = function (o) { + var feature = o.feature; + var location = feature.geometry.getBounds().getCenterLonLat(); + + feature.popup = new OpenLayers.Popup.FramedCloud( + feature.attributes.id, location, null, + "

" + feature.attributes.id + "

", + null, + feature.attributes.status !== "new", + function (e) { map.noteSelector.unselect(feature) } + ); + + map.addPopup(feature.popup); + // feature.popup.show(); + }; + + var noteUnselected = function (o) { + var feature = o.feature; + + map.removePopup(feature.popup); + + delete feature.popup; + }; + + var allowNoteReports = function () { + if (map.getZoom() > minZoom) { + newNoteControls.show(); + } else { + newNoteControls.hide(); + } + }; + + var addNote = function () { + var lonlat = map.getCenter(); + var layer = map.noteLayer; + var geometry = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat); + var feature = new OpenLayers.Feature.Vector(geometry, { + status: "new" + }); + + layer.addFeatures(feature); + map.noteSelector.unselectAll(); + map.noteSelector.select(feature); + map.noteMover.activate(); + map.noteLayer.setVisibility(true); + }; + + map.noteLayer = new OpenLayers.Layer.Vector("Notes", { + visibility: false, + projection: new OpenLayers.Projection("EPSG:4326"), + styleMap: new OpenLayers.StyleMap(new OpenLayers.Style({ + graphicWidth: 22, + graphicHeight: 22, + graphicOpacity: 0.7, + graphicXOffset: -11, + graphicYOffset: -11 + }, { + rules: [ + new OpenLayers.Rule({ + filter: new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.EQUAL_TO, + property: "status", + value: "new" + }), + symbolizer: { + externalGraphic: "<%= image_path 'new_note_marker.png' %>" + } + }), + new OpenLayers.Rule({ + filter: new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.EQUAL_TO, + property: "status", + value: "open" + }), + symbolizer: { + externalGraphic: "<%= image_path 'open_note_marker.png' %>" + } + }), + new OpenLayers.Rule({ + filter: new OpenLayers.Filter.Comparison({ + type: OpenLayers.Filter.Comparison.EQUAL_TO, + property: "status", + value: "closed" + }), + symbolizer: { + externalGraphic: "<%= image_path 'closed_note_marker.png' %>" + } + }) + ] + })), + strategies: [ + new OpenLayers.Strategy.BBOX() + ], + protocol: new OpenLayers.Protocol.HTTP({ + url: notesUrl, + format: new OpenLayers.Format.GeoJSON(), + callback: noteCallback + }) + }); + + map.noteLayer.events.register("beforefeaturesremoved", map, saveNewNotes); + map.noteLayer.events.register("featuresremoved", map, restoreNewNotes); + map.noteLayer.events.register("featureselected", map, noteSelected); + map.noteLayer.events.register("featureunselected", map, noteUnselected); + + map.addLayer(map.noteLayer); + + map.noteSelector = new OpenLayers.Control.SelectFeature(map.noteLayer, { + autoActivate: true + }); + + map.addControl(map.noteSelector); + + map.noteMover = new OpenLayers.Control.DragFeature(map.noteLayer, { + onDrag: function (feature, pixel) { + feature.popup.lonlat = feature.geometry.getBounds().getCenterLonLat(); + feature.popup.updatePosition(); + }, + featureCallbacks: { + over: function (feature) { + if (feature.attributes.status === "new") { + map.noteMover.overFeature.apply(map.noteMover, [feature]); + } + } + } + }); + + map.addControl(map.noteMover); + + newNoteControls.click(addNote); + + map.events.register("zoomend", map, allowNoteReports); + + return map.noteLayer; +} diff --git a/app/assets/stylesheets/common.css.scss b/app/assets/stylesheets/common.css.scss index c32144128..f2f16ec85 100644 --- a/app/assets/stylesheets/common.css.scss +++ b/app/assets/stylesheets/common.css.scss @@ -718,6 +718,23 @@ table.browse_details th { white-space: nowrap; } +td.browse_comments { + padding: 0px; +} + +td.browse_comments table { + border-collapse: collapse; +} + +td.browse_comments table td { + padding-bottom: 10px; +} + +td.browse_comments table td span.by { + font-size: small; + color: #999999; +} + #browse_map { width: 250px; } diff --git a/app/assets/stylesheets/large.css b/app/assets/stylesheets/large.css index 05da4445f..51e999924 100644 --- a/app/assets/stylesheets/large.css +++ b/app/assets/stylesheets/large.css @@ -17,3 +17,9 @@ .olControlZoom { display: none; } + +/* Rules for map bug reporting */ + +#reportbuganchor { + font-size: 150%; +} diff --git a/app/assets/stylesheets/notes.css b/app/assets/stylesheets/notes.css new file mode 100644 index 000000000..ee6198b18 --- /dev/null +++ b/app/assets/stylesheets/notes.css @@ -0,0 +1,42 @@ +.olPopupFramedCloudNotes dl { + margin: 0px; + padding: 0px; +} + +.olPopupFramedCloudNotes dt { + margin: 0px; + padding: 0px; + font-weight: bold; + float: left; + clear: left; +} + +.olPopupFramedCloudNotes dt:after { + content: ": "; +} + +.olPopupFramedCloudNotes dt { + margin-right: 1ex; +} + +.olPopupFramedCloudNotes dd { + margin: 0px; + padding: 0px; +} + +.olPopupFramedCloudNotes ul.buttons { + list-style-type: none; + padding: 0px; + margin: 0px; +} + +.olPopupFramedCloudNotes ul.buttons li { + display: inline; + margin: 0px; + padding: 0px; +} + +.olPopupFramedCloudNotes h3 { + font-size: 1.2em; + margin: 0.2em 0em 0.7em 0em; +} diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index f423c6388..73f0940d8 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -84,4 +84,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/notes_controller.rb b/app/controllers/notes_controller.rb new file mode 100644 index 000000000..fe3615d7a --- /dev/null +++ b/app/controllers/notes_controller.rb @@ -0,0 +1,330 @@ +class NotesController < 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 + + ## + # Return a list of notes in a given area + def index + # Figure out the bbox - we prefer a bbox argument but also + # support the old, deprecated, method with four arguments + if params[:bbox] + bbox = BoundingBox.from_bbox_params(params) + 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 = BoundingBox.from_lrbt_params(params) + end + + # Get any conditions that need to be applied + notes = closed_condition(Note.scoped) + + # Check that the boundaries are valid + bbox.check_boundaries + + # Check the the bounding box is not too big + bbox.check_size(MAX_NOTE_REQUEST_AREA) + + # Find the notes we want to return + @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments) + + # Render the result + respond_to do |format| + format.rss + format.xml + format.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 comment + # 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 feed + # Get any conditions that need to be applied + notes = closed_condition(Note.scoped) + + # Process any bbox + if params[:bbox] + bbox = BoundingBox.from_bbox_params(params) + + bbox.check_boundaries + bbox.check_size(MAX_NOTE_REQUEST_AREA) + + notes = notes.bbox(bbox) + end + + # Find the comments we want to return + @comments = NoteComment.where(:note_id => notes).order("created_at DESC").limit(result_limit).preload(:note) + + # Render the result + respond_to do |format| + format.rss + end + end + + ## + # Read a note + def show + # 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 + format.gpx + end + end + + ## + # Delete (hide) a note + def destroy + # 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 + @notes = closed_condition(Note.scoped) + @notes = @notes.joins(:comments).where("note_comments.body ~ ?", params[:q]) + + # Find the notes we want to return + @notes = @notes.order("updated_at DESC").limit(result_limit).preload(:comments) + + # Render the result + respond_to do |format| + format.rss { render :action => :index } + format.xml { render :action => :index } + format.json { render :action => :index } + format.gpx { render :action => :index } + end + end + + ## + # Display a list of notes by a specified user + def mine + if params[:display_name] + if @this_user = User.active.find_by_display_name(params[:display_name]) + @title = t 'note.mine.title', :user => @this_user.display_name + @heading = t 'note.mine.heading', :user => @this_user.display_name + @description = t 'note.mine.description', :user => render_to_string(:partial => "user", :object => @this_user) + @page = (params[:page] || 1).to_i + @page_size = 10 + @notes = @this_user.notes.order("updated_at DESC").offset((@page - 1) * @page_size).limit(@page_size).preload(:comments => :author) + else + @title = t 'user.no_such_user.title' + @not_found_user = params[:display_name] + + render :template => 'user/no_such_user', :status => :not_found + end + end + end + +private + #------------------------------------------------------------ + # utility functions below. + #------------------------------------------------------------ + + ## + # 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(notes) + if params[:closed] + closed_since = params[:closed].to_i + else + closed_since = 7 + end + + if closed_since < 0 + notes = notes.where("status != 'hidden'") + elsif closed_since > 0 + notes = notes.where("(status = 'open' OR (status = 'closed' AND closed_at > '#{Time.now - closed_since.days}'))") + else + notes = notes.where("status = 'open'") + end + + return notes + 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, :without_protection => true) + + 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/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3213c5e4c..e6a1e58e6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -107,6 +107,18 @@ module ApplicationHelper end end + def friendly_date(date) + content_tag(:span, time_ago_in_words(date), :title => l(date, :format => :friendly)) + end + + def note_author(object, link_options = {}) + if object.author.nil? + h(object.author_name) + else + link_to h(object.author_name), link_options.merge({:controller => "user", :action => "view", :display_name => object.author_name}) + end + end + private def javascript_strings_for_key(key) diff --git a/app/models/note.rb b/app/models/note.rb new file mode 100644 index 000000000..c32b1679b --- /dev/null +++ b/app/models/note.rb @@ -0,0 +1,81 @@ +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 + + attr_accessible :lat, :lon + + after_initialize :set_defaults + + # Sanity check the latitude and longitude and add an error if it's broken + def validate_position + errors.add(:base, "Note is not in the world") unless in_world? + 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 + +private + + # Fill in default values for new notes + def set_defaults + self.status = "open" unless self.attribute_present?(:status) + 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 343c3db22..2fb00c96f 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -114,6 +114,22 @@ class Notifier < ActionMailer::Base :subject => I18n.t('notifier.friend_notification.subject', :user => friend.befriender.display_name, :locale => @locale) 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 from_address(name, type, id, digest) diff --git a/app/models/user.rb b/app/models/user.rb index 68537e749..e126adb98 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,8 @@ class User < ActiveRecord::Base has_many :tokens, :class_name => "UserToken" has_many :preferences, :class_name => "UserPreference" has_many :changesets, :order => 'created_at DESC' + has_many :note_comments, :foreign_key => :author_id + has_many :notes, :through => :note_comments has_many :client_applications has_many :oauth_tokens, :class_name => "OauthToken", :order => "authorized_at desc", :include => [:client_application] diff --git a/app/views/browse/_map.html.erb b/app/views/browse/_map.html.erb index 2c29a0d00..d8ca30a15 100644 --- a/app/views/browse/_map.html.erb +++ b/app/views/browse/_map.html.erb @@ -5,14 +5,18 @@
- <% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible %> + <% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible? %>
<%= t 'browse.map.loading' %> + <% if map.instance_of? Note -%> + <%= link_to(t("browse.map.larger.area"), { :controller => :site, :action => :index, :notes => "yes" }, { :id => "area_larger_map", :class => "geolink bbox" }) %> + <% else -%> <%= link_to(t("browse.map.larger.area"), { :controller => :site, :action => :index, :box => "yes" }, { :id => "area_larger_map", :class => "geolink bbox" }) %> + <% end -%>
<%= link_to(h(t("browse.map.edit.area")) + content_tag(:span, "▾", :class => "menuicon"), { :controller => :site, :action => :edit }, { :id => "area_edit", :class => "geolink bbox" }) %> - <% unless map.instance_of? Changeset %> + <% unless map.instance_of? Changeset or map.instance_of? Note %>
<%= link_to(t("browse.map.larger." + map.class.to_s.downcase), { :controller => :site, :action => :index }, { :id => "object_larger_map", :class => "geolink object" }) %>
@@ -39,7 +43,7 @@
-<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible %> +<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible? %>