]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'upstream/pull/5686'
authorTom Hughes <tom@compton.nu>
Sun, 16 Feb 2025 11:12:47 +0000 (11:12 +0000)
committerTom Hughes <tom@compton.nu>
Sun, 16 Feb 2025 11:12:47 +0000 (11:12 +0000)
45 files changed:
app/abilities/ability.rb
app/abilities/api_ability.rb
app/assets/javascripts/index.js
app/assets/javascripts/index/home.js [new file with mode: 0644]
app/assets/stylesheets/errors.scss
app/controllers/accounts/homes_controller.rb [new file with mode: 0644]
app/controllers/api/changeset_comments_controller.rb
app/controllers/api/changesets_controller.rb
app/controllers/api/notes_controller.rb
app/controllers/api/user_blocks_controller.rb
app/controllers/changeset_comments/feeds_controller.rb
app/controllers/concerns/query_methods.rb [new file with mode: 0644]
app/controllers/users/lists_controller.rb
app/views/accounts/edit.html.erb
app/views/accounts/homes/show.html.erb [new file with mode: 0644]
app/views/api/changeset_comments/_changeset_comment.json.jbuilder [new file with mode: 0644]
app/views/api/changeset_comments/_changeset_comment.xml.builder [new file with mode: 0644]
app/views/api/changeset_comments/index.json.jbuilder [new file with mode: 0644]
app/views/api/changeset_comments/index.xml.builder [new file with mode: 0644]
app/views/api/changesets/_changeset.json.jbuilder
app/views/api/changesets/_changeset.xml.builder
app/views/layouts/_head.html.erb
app/views/layouts/_header.html.erb
app/views/layouts/_meta.html.erb
app/views/layouts/error.html.erb
app/views/layouts/map.html.erb
app/views/users/lists/_page.html.erb
app/views/users/lists/_user.html.erb
app/views/users/lists/show.html.erb
app/views/users/show.html.erb
config/locales/en.yml
config/routes.rb
config/settings.yml
config/settings/test.yml
lib/gpx.rb
lib/oauth.rb
test/application_system_test_case.rb
test/controllers/api/changeset_comments_controller_test.rb
test/controllers/api/notes_controller_test.rb
test/controllers/api/user_blocks_controller_test.rb
test/system/account_home_test.rb [new file with mode: 0644]
test/system/issues_test.rb
test/system/user_signup_test.rb
test/system/user_suspension_test.rb
test/teaspoon_env.rb

index adedce543e0f889ec519e07a378b26e40bd80a15..dd377a727c817d275fa97d5339aef32904c178ab 100644 (file)
@@ -29,7 +29,7 @@ class Ability
 
     if user&.active?
       can :welcome, :site
-      can :read, [:deletion, :account_terms, :account_pd_declaration]
+      can :read, [:deletion, :account_terms, :account_pd_declaration, :account_home]
 
       if Settings.status != "database_offline"
         can [:read, :create, :destroy], :changeset_subscription
index 7bbd9889ad53fbbb6d064b115f79eca7a54cad75..acacec049af1f20f6372c3a008945961fdda6c29 100644 (file)
@@ -11,6 +11,7 @@ class ApiAbility
       can :create, Note unless user
 
       can [:read, :download], Changeset
+      can :read, ChangesetComment
       can :read, Tracepoint
       can :read, User
       can :read, [Node, Way, Relation, OldNode, OldWay, OldRelation]
@@ -43,7 +44,9 @@ class ApiAbility
 
           can :destroy, Note if scopes.include?("write_notes")
 
-          can :redact, [OldNode, OldWay, OldRelation] if user&.terms_agreed? && scopes.include?("write_redactions")
+          can :redact, [OldNode, OldWay, OldRelation] if user.terms_agreed? && scopes.include?("write_redactions")
+
+          can :create, UserBlock if scopes.include?("write_blocks")
         end
       end
     end
index b6ead9f2d988eea2e391cd1d9689120a90a9f14e..810327e3fe15bd48a13dd4f5212bcb3bf932fbb7 100644 (file)
@@ -21,6 +21,7 @@
 //= require index/directions
 //= require index/changeset
 //= require index/query
+//= require index/home
 //= require router
 
 $(document).ready(function () {
@@ -38,6 +39,14 @@ $(document).ready(function () {
 
     $("#sidebar_loader").show().addClass("delayed-fade-in");
 
+    // Prevent caching the XHR response as a full-page URL
+    // https://github.com/openstreetmap/openstreetmap-website/issues/5663
+    if (content_path.indexOf("?") >= 0) {
+      content_path += "&xhr=1";
+    } else {
+      content_path += "?xhr=1";
+    }
+
     $("#sidebar_content")
       .empty();
 
@@ -213,16 +222,6 @@ $(document).ready(function () {
     L.marker([params.mlat, params.mlon]).addTo(map);
   }
 
-  $("#homeanchor").on("click", function (e) {
-    e.preventDefault();
-
-    var data = $(this).data(),
-        center = L.latLng(data.lat, data.lon);
-
-    map.setView(center, data.zoom);
-    L.marker(center, { icon: OSM.getUserIcon() }).addTo(map);
-  });
-
   function remoteEditHandler(bbox, object) {
     var remoteEditHost = "http://127.0.0.1:8111",
         osmHost = location.protocol + "//" + location.host,
@@ -357,7 +356,8 @@ $(document).ready(function () {
     "/relation/:id(/history)": OSM.Browse(map, "relation"),
     "/relation/:id/history/:version": OSM.OldBrowse(),
     "/changeset/:id": OSM.Changeset(map),
-    "/query": OSM.Query(map)
+    "/query": OSM.Query(map),
+    "/account/home": OSM.Home(map)
   });
 
   if (OSM.preferred_editor === "remote" && document.location.pathname === "/edit") {
diff --git a/app/assets/javascripts/index/home.js b/app/assets/javascripts/index/home.js
new file mode 100644 (file)
index 0000000..7e297b7
--- /dev/null
@@ -0,0 +1,38 @@
+OSM.Home = function (map) {
+  let marker;
+
+  function clearMarker() {
+    if (marker) map.removeLayer(marker);
+    marker = null;
+  }
+
+  const page = {};
+
+  page.pushstate = page.popstate = page.load = function () {
+    map.setSidebarOverlaid(true);
+    clearMarker();
+
+    if (OSM.home) {
+      OSM.router.withoutMoveListener(function () {
+        map.setView(OSM.home, 15, { reset: true });
+      });
+      marker = L.marker(OSM.home, {
+        icon: OSM.getUserIcon(),
+        title: I18n.t("javascripts.home.marker_title")
+      }).addTo(map);
+    } else {
+      $("#browse_status").html(
+        $("<div class='m-2 alert alert-warning'>").text(
+          I18n.t("javascripts.home.not_set")
+        )
+      );
+    }
+  };
+
+  page.unload = function () {
+    clearMarker();
+    $("#browse_status").empty();
+  };
+
+  return page;
+};
index fd140023218ec93014ca6ce1534e76f7f630ce2e..77b440a889261beb4be326d47782c102669312e1 100644 (file)
@@ -1,8 +1,43 @@
-.logo {
-  float: left;
-  margin: 10px;
+body {
+  margin: 1rem;
+  margin-top: 2rem;
+  font-family: system-ui;
 }
 
-.details {
-  float: left;
+main {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 1rem 2rem;
+  max-width: 960px;
+
+  .logo {
+    flex-shrink: 0;
+
+    img {
+      display: block;
+      max-width: 100%;
+      height: auto;
+    }
+  }
+
+  .details {
+    h1 {
+      margin-top: 0;
+    }
+  }
+}
+
+@media (min-width: 640px) {
+  body {
+    margin: 2rem;
+  }
+
+  main {
+    flex-direction: row;
+
+    .logo {
+      align-self: start;
+    }
+  }
 }
diff --git a/app/controllers/accounts/homes_controller.rb b/app/controllers/accounts/homes_controller.rb
new file mode 100644 (file)
index 0000000..e31cce7
--- /dev/null
@@ -0,0 +1,13 @@
+module Accounts
+  class HomesController < ApplicationController
+    layout :map_layout
+
+    before_action :authorize_web
+    before_action :set_locale
+    before_action :require_oauth
+
+    authorize_resource :class => :account_home
+
+    def show; end
+  end
+end
index c180571c58b10b0184e1f3dd98ab86f4fb5d8d27..808ac97ea3418257445177caeb8615cc2574ff78 100644 (file)
@@ -1,7 +1,9 @@
 module Api
   class ChangesetCommentsController < ApiController
-    before_action :check_api_writable
-    before_action :authorize
+    include QueryMethods
+
+    before_action :check_api_writable, :except => [:index]
+    before_action :authorize, :except => [:index]
 
     authorize_resource
 
@@ -9,6 +11,15 @@ module Api
 
     before_action :set_request_formats
 
+    ##
+    # show all comments or search for a subset
+    def index
+      @comments = ChangesetComment.includes(:author).where(:visible => true).order("created_at DESC")
+      @comments = query_conditions_time(@comments)
+      @comments = query_conditions_user(@comments, :author)
+      @comments = query_limit(@comments)
+    end
+
     ##
     # Add a comment to a changeset
     def create
index 9111bb609d27d91753d3b6b46a6ee7ae59954eae..3df7b75cea752aeb11a0681b728377d6f6a0d0e0 100644 (file)
@@ -2,6 +2,8 @@
 
 module Api
   class ChangesetsController < ApiController
+    include QueryMethods
+
     before_action :check_api_writable, :only => [:create, :update, :upload, :subscribe, :unsubscribe]
     before_action :setup_user_auth, :only => [:show]
     before_action :authorize, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
@@ -30,7 +32,7 @@ module Api
       changesets = conditions_bbox(changesets, bbox)
       changesets = conditions_user(changesets, params["user"], params["display_name"])
       changesets = conditions_time(changesets, params["time"])
-      changesets = conditions_from_to(changesets, params["from"], params["to"])
+      changesets = query_conditions_time(changesets)
       changesets = conditions_open(changesets, params["open"])
       changesets = conditions_closed(changesets, params["closed"])
       changesets = conditions_ids(changesets, params["changesets"])
@@ -43,7 +45,7 @@ module Api
                    end
 
       # limit the result
-      changesets = changesets.limit(result_limit)
+      changesets = query_limit(changesets)
 
       # preload users, tags and comments, and render result
       @changesets = changesets.preload(:user, :changeset_tags, :comments)
@@ -337,33 +339,6 @@ module Api
       raise OSM::APIBadUserInput, e.message.to_s
     end
 
-    ##
-    # restrict changesets to those opened during a particular time period
-    # works similar to from..to of notes controller, including the requirement of 'from' when specifying 'to'
-    def conditions_from_to(changesets, from, to)
-      if from
-        begin
-          from = Time.parse(from).utc
-        rescue ArgumentError
-          raise OSM::APIBadUserInput, "Date #{from} is in a wrong format"
-        end
-
-        begin
-          to = if to
-                 Time.parse(to).utc
-               else
-                 Time.now.utc
-               end
-        rescue ArgumentError
-          raise OSM::APIBadUserInput, "Date #{to} is in a wrong format"
-        end
-
-        changesets.where(:created_at => from..to)
-      else
-        changesets
-      end
-    end
-
     ##
     # return changesets which are open (haven't been closed yet)
     # we do this by seeing if the 'closed at' time is in the future. Also if we've
@@ -403,19 +378,5 @@ module Api
         changesets.where(:id => ids)
       end
     end
-
-    ##
-    # Get the maximum number of results to return
-    def result_limit
-      if params[:limit]
-        if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_changeset_query_limit
-          params[:limit].to_i
-        else
-          raise OSM::APIBadUserInput, "Changeset limit must be between 1 and #{Settings.max_changeset_query_limit}"
-        end
-      else
-        Settings.default_changeset_query_limit
-      end
-    end
   end
 end
index a0095d954b5d6f48dd891560d11c7f755523c964..af0c5e0398a4265ff6e122f927668e69b6910e9b 100644 (file)
@@ -1,5 +1,7 @@
 module Api
   class NotesController < ApiController
+    include QueryMethods
+
     before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
     before_action :setup_user_auth, :only => [:create, :show]
     before_action :authorize, :only => [:close, :reopen, :destroy, :comment]
@@ -36,7 +38,9 @@ module Api
       @max_lat = bbox.max_lat
 
       # Find the notes we want to return
-      @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
+      notes = notes.bbox(bbox).order("updated_at DESC")
+      notes = query_limit(notes)
+      @notes = notes.preload(:comments)
 
       # Render the result
       respond_to do |format|
@@ -234,8 +238,9 @@ module Api
 
       # Find the comments we want to return
       @comments = NoteComment.where(:note => notes)
-                             .order(:created_at => :desc).limit(result_limit)
-                             .preload(:author, :note => { :comments => :author })
+                             .order(:created_at => :desc)
+      @comments = query_limit(@comments)
+      @comments = @comments.preload(:author, :note => { :comments => :author })
 
       # Render the result
       respond_to do |format|
@@ -251,19 +256,8 @@ module Api
       @notes = bbox_condition(@notes)
 
       # Add any user filter
-      if params[:display_name] || params[:user]
-        if params[:display_name]
-          @user = User.find_by(:display_name => params[:display_name])
-
-          raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
-        else
-          @user = User.find_by(:id => params[:user])
-
-          raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
-        end
-
-        @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
-      end
+      user = query_conditions_user_value
+      @notes = @notes.joins(:comments).where(:note_comments => { :author_id => user }) if user
 
       # Add any text filter
       if params[:q]
@@ -271,29 +265,12 @@ module Api
       end
 
       # Add any date filter
-      if params[:from]
-        begin
-          from = Time.parse(params[:from]).utc
-        rescue ArgumentError
-          raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
-        end
-
-        begin
-          to = if params[:to]
-                 Time.parse(params[:to]).utc
-               else
-                 Time.now.utc
-               end
-        rescue ArgumentError
-          raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
-        end
-
-        @notes = if params[:sort] == "updated_at"
-                   @notes.where(:updated_at => from..to)
-                 else
-                   @notes.where(:created_at => from..to)
-                 end
-      end
+      time_filter_property = if params[:sort] == "updated_at"
+                               :updated_at
+                             else
+                               :created_at
+                             end
+      @notes = query_conditions_time(@notes, time_filter_property)
 
       # Choose the sort order
       @notes = if params[:sort] == "created_at"
@@ -311,7 +288,8 @@ module Api
                end
 
       # Find the notes we want to return
-      @notes = @notes.distinct.limit(result_limit).preload(:comments)
+      @notes = query_limit(@notes.distinct)
+      @notes = @notes.preload(:comments)
 
       # Render the result
       respond_to do |format|
@@ -328,20 +306,6 @@ module Api
     # utility functions below.
     #------------------------------------------------------------
 
-    ##
-    # Get the maximum number of results to return
-    def result_limit
-      if params[:limit]
-        if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
-          params[:limit].to_i
-        else
-          raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
-        end
-      else
-        Settings.default_note_query_limit
-      end
-    end
-
     ##
     # Generate a condition to choose which notes we want based
     # on their status and the user's request parameters
index 51f0d26d3e641ee3762357a20a8b085eea505e32..e1fb70a659f3ddea3fee902e7b846e68c3bdbe83 100644 (file)
@@ -1,5 +1,8 @@
 module Api
   class UserBlocksController < ApiController
+    before_action :check_api_writable, :only => :create
+    before_action :authorize, :only => :create
+
     authorize_resource
 
     before_action :set_request_formats
@@ -11,5 +14,33 @@ module Api
     rescue ActiveRecord::RecordNotFound
       raise OSM::APINotFoundError
     end
+
+    def create
+      raise OSM::APIBadUserInput, "No user was given" unless params[:user]
+
+      user = User.visible.find_by(:id => params[:user])
+      raise OSM::APINotFoundError unless user
+      raise OSM::APIBadUserInput, "No reason was given" unless params[:reason]
+      raise OSM::APIBadUserInput, "No period was given" unless params[:period]
+
+      period = Integer(params[:period], :exception => false)
+      raise OSM::APIBadUserInput, "Period should be a number of hours" unless period
+
+      max_period = UserBlock::PERIODS.max
+      raise OSM::APIBadUserInput, "Period must be between 0 and #{max_period}" if period.negative? || period > max_period
+      raise OSM::APIBadUserInput, "Needs_view must be true if provided" unless params[:needs_view].nil? || params[:needs_view] == "true"
+
+      ends_at = Time.now.utc + period.hours
+      needs_view = params[:needs_view] == "true"
+      @user_block = UserBlock.create(
+        :user => user,
+        :creator => current_user,
+        :reason => params[:reason],
+        :ends_at => ends_at,
+        :deactivates_at => (ends_at unless needs_view),
+        :needs_view => needs_view
+      )
+      render :show
+    end
   end
 end
index fef48bb188f885dbfac1188f0541002f16bba88b..148873eea8321b338d54dfd9a51b37045fd1a97e 100644 (file)
@@ -1,5 +1,7 @@
 module ChangesetComments
   class FeedsController < ApplicationController
+    include QueryMethods
+
     before_action :authorize_web
     before_action :set_locale
 
@@ -19,10 +21,13 @@ module ChangesetComments
         changeset = Changeset.find(changeset_id)
 
         # Return comments for this changeset only
-        @comments = changeset.comments.includes(:author, :changeset).reverse_order.limit(comments_limit)
+        @comments = changeset.comments.includes(:author, :changeset).reverse_order
+        @comments = query_limit(@comments)
       else
         # Return comments
-        @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC").limit(comments_limit).preload(:changeset)
+        @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC")
+        @comments = query_limit(@comments)
+        @comments = @comments.preload(:changeset)
       end
 
       # Render the result
@@ -32,21 +37,5 @@ module ChangesetComments
     rescue OSM::APIBadUserInput
       head :bad_request
     end
-
-    private
-
-    ##
-    # Get the maximum number of comments to return
-    def comments_limit
-      if params[:limit]
-        if params[:limit].to_i.positive? && params[:limit].to_i <= 10000
-          params[:limit].to_i
-        else
-          raise OSM::APIBadUserInput, "Comments limit must be between 1 and 10000"
-        end
-      else
-        100
-      end
-    end
   end
 end
diff --git a/app/controllers/concerns/query_methods.rb b/app/controllers/concerns/query_methods.rb
new file mode 100644 (file)
index 0000000..eb06842
--- /dev/null
@@ -0,0 +1,92 @@
+module QueryMethods
+  extend ActiveSupport::Concern
+
+  private
+
+  ##
+  # Filter the resulting items by user
+  def query_conditions_user(items, filter_property)
+    user = query_conditions_user_value
+    items = items.where(filter_property => user) if user
+    items
+  end
+
+  ##
+  # Get user value for query filtering by user
+  # Raises OSM::APIBadUserInput if user not found like notes api does, changesets api raises OSM::APINotFoundError instead
+  def query_conditions_user_value
+    if params[:display_name] || params[:user]
+      if params[:display_name]
+        user = User.find_by(:display_name => params[:display_name])
+
+        raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless user
+      else
+        user = User.find_by(:id => params[:user])
+
+        raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless user
+      end
+
+      user
+    end
+  end
+
+  ##
+  # Restrict the resulting items to those created during a particular time period
+  # Using 'to' requires specifying 'from' as well for historical reasons
+  def query_conditions_time(items, filter_property = :created_at)
+    interval = query_conditions_time_value
+
+    if interval
+      items.where(filter_property => interval)
+    else
+      items
+    end
+  end
+
+  ##
+  # Get query time interval from request parameters or nil
+  def query_conditions_time_value
+    if params[:from]
+      begin
+        from = Time.parse(params[:from]).utc
+      rescue ArgumentError
+        raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
+      end
+
+      begin
+        to = if params[:to]
+               Time.parse(params[:to]).utc
+             else
+               Time.now.utc
+             end
+      rescue ArgumentError
+        raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
+      end
+
+      from..to
+    end
+  end
+
+  ##
+  # Limit the result according to request parameters and settings
+  def query_limit(items)
+    items.limit(query_limit_value)
+  end
+
+  ##
+  # Get query limit value from request parameters and settings
+  def query_limit_value
+    name = controller_path.sub(%r{^api/}, "").tr("/", "_").singularize
+    max_limit = Settings["max_#{name}_query_limit"]
+    default_limit = Settings["default_#{name}_query_limit"]
+    if params[:limit]
+      if params[:limit].to_i.positive? && params[:limit].to_i <= max_limit
+        params[:limit].to_i
+      else
+        raise OSM::APIBadUserInput, "#{controller_name.classify} limit must be between 1 and #{max_limit}"
+      end
+    else
+      default_limit
+    end
+  end
+end
index a5cd7203d715b4bd1629f5c942152d2de0ec84eb..a2f35e9b213a8f5ea91a4a8ecbbe76ca1e889c97 100644 (file)
@@ -16,8 +16,8 @@ module Users
       @params = params.permit(:status, :ip, :before, :after)
 
       users = User.all
-      users = users.where(:status => @params[:status]) if @params[:status]
-      users = users.where(:creation_address => @params[:ip]) if @params[:ip]
+      users = users.where(:status => @params[:status]) if @params[:status].present?
+      users = users.where("creation_address <<= ?", @params[:ip]) if @params[:ip].present?
 
       @users_count = users.limit(501).count
       @users_count = I18n.t("count.at_least_pattern", :count => 500) if @users_count > 500
index ce7dd97a83d4dd7597810559c4d2fa8452f2b56b..5c626fc9f985645120e6cda69ce6f4ab1db46bc6 100644 (file)
@@ -3,7 +3,7 @@
 <% end %>
 
 <% content_for :heading do %>
-  <h1><%= t ".my settings" %></h1>
+  <h1><%= t ".my_account" %></h1>
 <% end %>
 
 <%= render :partial => "settings_menu" %>
diff --git a/app/views/accounts/homes/show.html.erb b/app/views/accounts/homes/show.html.erb
new file mode 100644 (file)
index 0000000..ea6ee70
--- /dev/null
@@ -0,0 +1 @@
+<% content_for(:content_class) { "overlay-sidebar" } %>
diff --git a/app/views/api/changeset_comments/_changeset_comment.json.jbuilder b/app/views/api/changeset_comments/_changeset_comment.json.jbuilder
new file mode 100644 (file)
index 0000000..73b1ec9
--- /dev/null
@@ -0,0 +1,8 @@
+json.id changeset_comment.id
+json.visible changeset_comment.visible
+json.date changeset_comment.created_at.xmlschema
+if changeset_comment.author.data_public?
+  json.uid changeset_comment.author.id
+  json.user changeset_comment.author.display_name
+end
+json.text changeset_comment.body
diff --git a/app/views/api/changeset_comments/_changeset_comment.xml.builder b/app/views/api/changeset_comments/_changeset_comment.xml.builder
new file mode 100644 (file)
index 0000000..951556f
--- /dev/null
@@ -0,0 +1,12 @@
+cattrs = {
+  "id" => changeset_comment.id,
+  "date" => changeset_comment.created_at.xmlschema,
+  "visible" => changeset_comment.visible
+}
+if changeset_comment.author.data_public?
+  cattrs["uid"] = changeset_comment.author.id
+  cattrs["user"] = changeset_comment.author.display_name
+end
+xml.comment(cattrs) do |comment_xml_node|
+  comment_xml_node.text(changeset_comment.body)
+end
diff --git a/app/views/api/changeset_comments/index.json.jbuilder b/app/views/api/changeset_comments/index.json.jbuilder
new file mode 100644 (file)
index 0000000..0286b1a
--- /dev/null
@@ -0,0 +1,5 @@
+json.partial! "api/root_attributes"
+
+json.comments(@comments) do |comment|
+  json.partial! comment
+end
diff --git a/app/views/api/changeset_comments/index.xml.builder b/app/views/api/changeset_comments/index.xml.builder
new file mode 100644 (file)
index 0000000..cfa59c9
--- /dev/null
@@ -0,0 +1,7 @@
+xml.instruct! :xml, :version => "1.0"
+
+xml.osm(OSM::API.new.xml_root_attributes) do |osm|
+  @comments.includes(:author).each do |comment|
+    osm << render(comment)
+  end
+end
index f0e46132008be266b51408ab0b948b649f8fdfc6..7001a95e854ababb5a4293a4c011b5e8c53f7baa 100644 (file)
@@ -23,13 +23,6 @@ json.tags changeset.tags unless changeset.tags.empty?
 
 if @comments
   json.comments(@comments) do |comment|
-    json.id comment.id
-    json.visible comment.visible
-    json.date comment.created_at.xmlschema
-    if comment.author.data_public?
-      json.uid comment.author.id
-      json.user comment.author.display_name
-    end
-    json.text comment.body
+    json.partial! comment
   end
 end
index 08cfbbc79bc9d66065ef6b6ca3362502fe32ecae..072f8fc5d613fc26b011b4b3b4b17ab9a2598c21 100644 (file)
@@ -27,18 +27,7 @@ xml.changeset(attrs) do |changeset_xml_node|
   if @comments
     changeset_xml_node.discussion do |discussion_xml_node|
       @comments.each do |comment|
-        cattrs = {
-          "id" => comment.id,
-          "date" => comment.created_at.xmlschema,
-          "visible" => comment.visible
-        }
-        if comment.author.data_public?
-          cattrs["uid"] = comment.author.id
-          cattrs["user"] = comment.author.display_name
-        end
-        discussion_xml_node.comment(cattrs) do |comment_xml_node|
-          comment_xml_node.text(comment.body)
-        end
+        discussion_xml_node << render(comment)
       end
     end
   end
index e6d709b27f7de74192a36af8760c9c96dc88febe..37d830ef67f3cc2e4ef0174afad8fb77ddeb60ad 100644 (file)
@@ -1,6 +1,5 @@
 <%= tag.head :data => application_data do %>
-  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <%= render :partial => "layouts/meta" %>
   <%= javascript_include_tag "turbo", :type => "module" %>
   <%= javascript_include_tag "application" %>
   <%= javascript_include_tag "i18n/#{I18n.locale}" %>
@@ -11,7 +10,6 @@
   <% end %>
   <%= stylesheet_link_tag "print-#{dir}", :media => "print" %>
   <%= stylesheet_link_tag "leaflet-all", :media => "screen, print" %>
-  <%= render :partial => "layouts/meta" %>
   <%= yield :head %>
   <%= yield :auto_discovery_link_tag %>
   <%= csrf_meta_tag %>
index aa524938181c32cdb210fdb1c1cb9168b056b58d..f09812972ef31ff234233072891f20a0bb1d4300 100644 (file)
             <span class='badge count-number'><%= number_with_delimiter(current_user.new_messages.size) %></span>
           <% end %>
           <%= link_to t("users.show.my profile"), current_user, :class => "dropdown-item" %>
-          <%= link_to t("users.show.my settings"), edit_account_path, :class => "dropdown-item" %>
+          <%= link_to t("users.show.my_account"), edit_account_path, :class => "dropdown-item" %>
           <%= link_to t("users.show.my_preferences"), preferences_path, :class => "dropdown-item" %>
           <div class="dropdown-divider"></div>
-          <%= yield :greeting %>
+          <% if current_user.home_location? %>
+            <%= link_to t("layouts.home"), account_home_path, :class => "dropdown-item" %>
+          <% end %>
           <%= link_to t("layouts.logout"), logout_path(:referer => request.fullpath), :method => "post", :class => "geolink dropdown-item" %>
         </div>
       </div>
index 4c88887f920d4462c1650759a3801c4d0ccb4ec5..48be6e0aa1ee86b31f724c896084febfa4aabb30 100644 (file)
@@ -1,3 +1,5 @@
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1">
 <% [57, 60, 72, 76, 114, 120, 144, 152, 180].each do |size| -%>
 <%= favicon_link_tag "apple-touch-icon-#{size}x#{size}.png", :rel => "apple-touch-icon", :sizes => "#{size}x#{size}", :type => "image/png" %>
 <% end -%>
index dfcb3cb91c27a7924ded612df5a5596b71c9cb7a..eab764aa3a2a7e0a3a0ed3e7727b774687ab5b35 100644 (file)
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
+<html lang="<%= I18n.locale %>" dir="<%= dir %>">
   <head>
     <meta charset="utf-8">
     <title>OpenStreetMap</title>
@@ -7,11 +7,13 @@
     <%= render :partial => "layouts/meta" %>
   </head>
   <body>
-    <a href="<%= root_path %>">
-      <%= image_tag "osm_logo.svg", :alt => t("layouts.logo.alt_text"), :class => "logo" %>
-    </a>
-    <div class="details">
-      <%= yield %>
-    </div>
+    <main>
+      <a href="<%= root_path %>" class="logo">
+        <%= image_tag "osm_logo.svg", :alt => t("layouts.logo.alt_text") %>
+      </a>
+      <div class="details">
+        <%= yield %>
+      </div>
+    </main>
   </body>
 </html>
index e17ea4ed826bf2f9593973f766dd76f8ba54d4c0..72f6076b45850999df682013efd1b0e9e60b2aa5 100644 (file)
@@ -4,18 +4,6 @@
 
 <% content_for(:body_class) { "map-layout" } %>
 
-<% if current_user&.home_location? %>
-  <% content_for :greeting do %>
-    <%= link_to t("layouts.home"),
-                "#",
-                :id => "homeanchor",
-                :class => "set_position dropdown-item",
-                :data => { :lat => current_user.home_lat,
-                           :lon => current_user.home_lon,
-                           :zoom => 15 } %>
-  <% end %>
-<% end %>
-
 <% content_for :header do %>
   <%= render :partial => "layouts/search", :locals => { :autofocus => false } %>
 <% end %>
index d06516e06999009428ccc7d437898a8781fc8a9f..2c06f02375230e55d17d930f3ca7bf617cc1a16d 100644 (file)
@@ -1,48 +1,52 @@
 <turbo-frame id="pagination" target="_top" data-turbo="false">
-  <%= form_tag @params, :method => :put do %>
-    <div class="row">
-      <div class="col">
-        <%= render "shared/pagination",
-                   :translation_scope => "shared.pagination.users",
-                   :newer_id => @newer_users_id,
-                   :older_id => @older_users_id %>
-      </div>
-      <div class="col col-auto">
-        <%= t ".found_users", :count => @users_count %>
-      </div>
-    <div>
+  <% unless @users.empty? %>
+    <%= form_tag @params, :method => :put do %>
+      <div class="row">
+        <div class="col">
+          <%= render "shared/pagination",
+                     :translation_scope => "shared.pagination.users",
+                     :newer_id => @newer_users_id,
+                     :older_id => @older_users_id %>
+        </div>
+        <div class="col col-auto">
+          <%= t ".found_users", :count => @users_count %>
+        </div>
+      <div>
 
-    <%= hidden_field_tag :status, params[:status] if params[:status] %>
-    <%= hidden_field_tag :ip, params[:ip] if params[:ip] %>
-    <%= hidden_field_tag :page, params[:page] if params[:page] %>
-    <table id="user_list" class="table table-borderless table-striped">
-      <thead>
-        <tr>
-          <td colspan="2">
-          </td>
-          <td>
-            <%= check_box_tag "user_all", "1", false %>
-          </td>
-        </tr>
-      </thead>
-      <%= render :partial => "user", :collection => @users %>
-    </table>
+      <%= hidden_field_tag :status, params[:status] if params[:status] %>
+      <%= hidden_field_tag :ip, params[:ip] if params[:ip] %>
+      <%= hidden_field_tag :page, params[:page] if params[:page] %>
+      <table id="user_list" class="table table-borderless table-striped">
+        <thead>
+          <tr>
+            <td colspan="2">
+            </td>
+            <td>
+              <%= check_box_tag "user_all", "1", false %>
+            </td>
+          </tr>
+        </thead>
+        <%= render :partial => "user", :collection => @users %>
+      </table>
 
-    <div class="row">
-      <div class="col">
-        <%= render "shared/pagination",
-                   :translation_scope => "shared.pagination.users",
-                   :newer_id => @newer_users_id,
-                   :older_id => @older_users_id %>
-      </div>
-      <div class="col col-auto">
-        <%= t ".found_users", :count => @users_count %>
-      </div>
-    <div>
+      <div class="row">
+        <div class="col">
+          <%= render "shared/pagination",
+                     :translation_scope => "shared.pagination.users",
+                     :newer_id => @newer_users_id,
+                     :older_id => @older_users_id %>
+        </div>
+        <div class="col col-auto">
+          <%= t ".found_users", :count => @users_count %>
+        </div>
+      <div>
 
-    <div>
-      <%= submit_tag t(".confirm"), :name => "confirm", :class => "btn btn-primary" %>
-      <%= submit_tag t(".hide"), :name => "hide", :class => "btn btn-primary" %>
-    </div>
-  <% end %>
+      <div>
+        <%= submit_tag t(".confirm"), :name => "confirm", :class => "btn btn-primary" %>
+        <%= submit_tag t(".hide"), :name => "hide", :class => "btn btn-primary" %>
+      </div>
+    <% end %>
+  <% else -%>
+    <p><%= t ".empty" %></p>
+  <% end -%>
 </turbo-frame>
index 14216a24449f38923e8ababe11b2aa9738d638c5..a3900cb382eb2f46b8402525878be1910597eabe 100644 (file)
@@ -7,7 +7,7 @@
       <% if user.creation_address %>
         <%= t ".summary_html",
               :name => link_to(user.display_name, user),
-              :ip_address => link_to(user.creation_address, :ip => user.creation_address),
+              :ip_address => link_to(user.creation_address, :status => params[:status], :ip => user.creation_address),
               :date => l(user.created_at, :format => :friendly) %>
       <% else %>
         <%= t ".summary_no_ip_html",
index abf0092cebb5c8c695c7369406719b85e64e05c8..dd037c7af6c3a13dee647bf9ba3e077ca658be7e 100644 (file)
@@ -8,8 +8,26 @@
   <h1><%= t(".heading") %></h1>
 <% end %>
 
-<% unless @users.empty? %>
-  <%= render :partial => "page" %>
-<% else %>
-  <p><%= t ".empty" %></p>
-<% end %>
+<%= form_tag(users_list_path, :method => :get, :data => { "turbo" => true, "turbo-frame" => "pagination", "turbo-action" => "advance" }) do %>
+  <div class="row gx-1">
+    <div class="mb-3 col-md-auto">
+      <%= select_tag :status,
+                     options_for_select(User.aasm.states.map(&:name).map { |state| [t(".states.#{state}"), state] }, params[:status]),
+                     :include_blank => t(".select_status"),
+                     :data => { :behavior => "category_dropdown" },
+                     :class => "form-select" %>
+    </div>
+    <div class="mb-3 col-md">
+      <%= text_field_tag :ip,
+                         params[:ip],
+                         :placeholder => t(".ip_address"),
+                         :autocomplete => "on",
+                         :class => "form-control" %>
+    </div>
+    <div class="mb-3 col-md-auto">
+      <%= submit_tag t(".search"), :name => nil, :class => "btn btn-primary" %>
+    </div>
+  </div>
+<% end -%>
+
+<%= render :partial => "page" %>
index 8ef7b56447d4fd891cdcb60daa0a8ef67c7543f9..a32f5fae1e071337716f945fbd5b531093cd3e92 100644 (file)
@@ -30,7 +30,7 @@
               <span class='badge count-number'><%= number_with_delimiter(current_user.diary_comments.size) %></span>
             </li>
             <li>
-              <%= link_to t(".my settings"), edit_account_path %>
+              <%= link_to t(".my_account"), edit_account_path %>
             </li>
 
             <% if current_user.blocks.exists? %>
index eaf0fdf1582d0473a180b32a0342dc7c30deedd6..42669ca9e3f5ba19ac77998c7cc3b0130dc7aa0f 100644 (file)
@@ -246,7 +246,7 @@ en:
   accounts:
     edit:
       title: "Edit account"
-      my settings: My Settings
+      my_account: My Account
       current email address: "Current Email Address"
       external auth: "External Authentication"
       openid:
@@ -1922,7 +1922,7 @@ en:
       failure: Couldn't update profile.
   sessions:
     new:
-      tab_title: "Log in"
+      tab_title: "Log In"
       login_to_authorize_html: "Log in to OpenStreetMap to access %{client_app_name}."
       email or username: "Email Address or Username"
       password: "Password"
@@ -2653,8 +2653,8 @@ en:
       need_to_see_terms: "Your access to the API is temporarily suspended. Please log-in to the web interface to view the Contributor Terms. You do not need to agree, but you must view them."
     settings_menu:
       account_settings: Account Settings
-      oauth2_applications: OAuth 2 applications
-      oauth2_authorizations: OAuth 2 authorizations
+      oauth2_applications: OAuth 2 Applications
+      oauth2_authorizations: OAuth 2 Authorizations
       muted_users: Muted Users
     auth_providers:
       openid_url: "OpenID URL"
@@ -2713,6 +2713,7 @@ en:
       write_gpx: Upload GPS traces
       write_notes: Modify notes
       write_redactions: Redact map data
+      write_blocks: Create and revoke user blocks
       read_email: Read user email address
       consume_messages: Read, update status and delete user messages
       send_messages: Send private messages to other users
@@ -2770,7 +2771,7 @@ en:
   users:
     new:
       title: "Sign Up"
-      tab_title: "Sign up"
+      tab_title: "Sign Up"
       signup_to_authorize_html: "Sign up with OpenStreetMap to access %{client_app_name}."
       no_auto_account_create: "Unfortunately we are not currently able to create an account for you automatically."
       please_contact_support_html: 'Please contact %{support_link} to arrange for an account to be created - we will try and deal with the request as quickly as possible.'
@@ -2809,7 +2810,7 @@ en:
       my notes: My Notes
       my messages: My Messages
       my profile: My Profile
-      my settings: My Settings
+      my_account: My Account
       my comments: My Comments
       my_preferences: My Preferences
       my_dashboard: My Dashboard
@@ -2889,13 +2890,22 @@ en:
       show:
         title: Users
         heading: Users
-        empty: No matching users found
+        select_status: Select Status
+        states:
+          pending: Pending
+          active: Active
+          confirmed: Confirmed
+          suspended: Suspended
+          deleted: Deleted
+        ip_address: IP Address
+        search: Search
       page:
         found_users:
           one: "%{count} user found"
           other: "%{count} users found"
         confirm: Confirm Selected Users
         hide: Hide Selected Users
+        empty: No matching users found
       user:
         summary_html: "%{name} created from %{ip_address} on %{date}"
         summary_no_ip_html: "%{name} created on %{date}"
@@ -2903,7 +2913,7 @@ en:
       index:
         heading_html: "%{user}'s Comments"
         changesets: "Changesets"
-        diary_entries: "Diary entries"
+        diary_entries: "Diary Entries"
         no_comments: "No comments"
     changeset_comments:
       index:
@@ -3316,6 +3326,9 @@ en:
       show_address: Show address
       query_features: Query features
       centre_map: Centre map here
+    home:
+      marker_title: My home location
+      not_set: Home location is not set for your account
   redactions:
     edit:
       heading: "Edit Redaction"
index 3971494aa155d1851cb63164283ee124d4bd54ec..0ffd0a546c431552257073e2e206759da2901c11 100644 (file)
@@ -38,6 +38,8 @@ OpenStreetMap::Application.routes.draw do
   end
 
   namespace :api, :path => "api/0.6" do
+    resources :changeset_comments, :only => :index
+
     resources :nodes, :only => [:index, :create]
     resources :nodes, :path => "node", :id => /\d+/, :only => [:show, :update, :destroy] do
       scope :module => :nodes do
@@ -121,7 +123,7 @@ OpenStreetMap::Application.routes.draw do
       resource :subscription, :only => [:create, :destroy], :controller => "note_subscriptions"
     end
 
-    resources :user_blocks, :only => :show, :id => /\d+/, :controller => "user_blocks"
+    resources :user_blocks, :only => [:show, :create], :id => /\d+/, :controller => "user_blocks"
     namespace :user_blocks, :path => "user/blocks" do
       resource :active_list, :path => "active", :only => :show
     end
@@ -300,6 +302,7 @@ OpenStreetMap::Application.routes.draw do
       resource :terms, :only => [:show, :update]
       resource :pd_declaration, :only => [:show, :create]
       resource :deletion, :only => :show
+      resource :home, :only => :show
     end
   end
 
index 2e0346f0031c891d226deeddaa284adb2801cc31..51f4444c4802edce9b6cc2c5869a8f0c4f8a33ed 100644 (file)
@@ -35,6 +35,14 @@ tracepoints_per_page: 5000
 default_changeset_query_limit: 100
 # Maximum limit on the number of changesets returned by the changeset query api method
 max_changeset_query_limit: 100
+# Default limit on the number of changeset comments returned by the api
+default_changeset_comment_query_limit: 100
+# Maximum limit on the number of changesets comments returned by the api
+max_changeset_comment_query_limit: 10000
+# Default limit on the number of changeset comments in feeds
+default_changeset_comments_feed_query_limit: 100
+# Maximum limit on the number of changesets comments in feeds
+max_changeset_comments_feed_query_limit: 10000
 # Maximum number of nodes that will be returned by the api in a map request
 max_number_of_nodes: 50000
 # Maximum number of nodes that can be in a way (checked on save)
index b0e2f461392cce37eb7221ad9f72e0601f7a361c..3cf8c2836284c5b29fd03251753ea5f25aa3b2aa 100644 (file)
@@ -53,3 +53,7 @@ doorkeeper_signing_key: |
   cK1+/2V+OkM/0nXjxPwPj7LiOediUyZNUn48r29uGOL1S83PSUdyST207CP6mZjc
   K8aJmnGsVEAcWPzbpNh14q/c
   -----END PRIVATE KEY-----
+# Run system tests using headless Firefox
+system_test_headless: true
+# Override Firefox binary used in system tests
+#system_test_firefox_binary:
index 45a4dcf5fc0bd62335ea0339409e2c1fbfd49e6d..921dce12cf51c557c8f41f3530de61f87a8a33ca 100644 (file)
@@ -31,6 +31,8 @@ module GPX
             point.altitude ||= 0
             yield point
             @actual_points += 1
+            @lats << point.latitude
+            @lons << point.longitude
           elsif reader.name == "trkseg"
             @tracksegs += 1
           end
@@ -44,6 +46,8 @@ module GPX
       @possible_points = 0
       @actual_points = 0
       @tracksegs = 0
+      @lats = []
+      @lons = []
 
       begin
         Archive::Reader.open_filename(@file).each_entry_with_data do |entry, data|
@@ -94,9 +98,9 @@ module GPX
 
           first = true
 
-          points.each_with_index do |p, pt|
-            px = proj.x(p.longitude)
-            py = proj.y(p.latitude)
+          @actual_points.times do |pt|
+            px = proj.x @lons[pt]
+            py = proj.y @lats[pt]
 
             if (pt >= (points_per_frame * n)) && (pt <= (points_per_frame * (n + 1)))
               pen.thickness = 3
@@ -151,9 +155,9 @@ module GPX
 
         first = true
 
-        points do |p|
-          px = proj.x(p.longitude)
-          py = proj.y(p.latitude)
+        @actual_points.times do |pt|
+          px = proj.x @lons[pt]
+          py = proj.y @lats[pt]
 
           pen.line(px, py, oldpx, oldpy) unless first
 
index dfa3a8028f2ac98d818081782dc489eb05ba4f1e..47edba5005244ecd5367429db818b5afac2cd881 100644 (file)
@@ -1,11 +1,11 @@
 module Oauth
   SCOPES = %w[
     read_prefs write_prefs write_diary
-    write_api write_changeset_comments read_gpx write_gpx write_notes write_redactions
+    write_api write_changeset_comments read_gpx write_gpx write_notes write_redactions write_blocks
     consume_messages send_messages openid
   ].freeze
   PRIVILEGED_SCOPES = %w[read_email skip_authorization].freeze
-  MODERATOR_SCOPES = %w[write_redactions].freeze
+  MODERATOR_SCOPES = %w[write_redactions write_blocks].freeze
 
   class Scope
     attr_reader :name
index 0ddb8a87ad73f91606df90af224df851c9919c06..f2fcadfd7c582efedfdcd60df7ee0947b896db4b 100644 (file)
@@ -7,8 +7,9 @@ ActiveSupport.on_load(:action_dispatch_system_test_case) do
 end
 
 class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
-  driven_by :selenium, :using => :headless_firefox do |options|
+  driven_by :selenium, :using => Settings.system_test_headless ? :headless_firefox : :firefox do |options|
     options.add_preference("intl.accept_languages", "en")
+    options.binary = Settings.system_test_firefox_binary if Settings.system_test_firefox_binary
   end
 
   def before_setup
@@ -45,4 +46,8 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
   def within_content_body(&)
     within("#content > .content-body", &)
   end
+
+  def within_content_heading(&)
+    within("#content > .content-heading", &)
+  end
 end
index e456a3ca416f4cf25113827d0dd7c6b302c7746b..ba4200d3f8d2cfda3f6979b71a8fb74fb5eba0f8 100644 (file)
@@ -5,6 +5,14 @@ module Api
     ##
     # test all routes which lead to this controller
     def test_routes
+      assert_routing(
+        { :path => "/api/0.6/changeset_comments", :method => :get },
+        { :controller => "api/changeset_comments", :action => "index" }
+      )
+      assert_routing(
+        { :path => "/api/0.6/changeset_comments.json", :method => :get },
+        { :controller => "api/changeset_comments", :action => "index", :format => "json" }
+      )
       assert_routing(
         { :path => "/api/0.6/changeset/1/comment", :method => :post },
         { :controller => "api/changeset_comments", :action => "create", :id => "1" }
@@ -31,6 +39,46 @@ module Api
       )
     end
 
+    def test_index
+      user1 = create(:user)
+      user2 = create(:user)
+      changeset1 = create(:changeset, :closed, :user => user2)
+      comment11 = create(:changeset_comment, :changeset => changeset1, :author => user1, :created_at => "2023-01-01", :body => "changeset 1 question")
+      comment12 = create(:changeset_comment, :changeset => changeset1, :author => user2, :created_at => "2023-02-01", :body => "changeset 1 answer")
+      changeset2 = create(:changeset, :closed, :user => user1)
+      comment21 = create(:changeset_comment, :changeset => changeset2, :author => user1, :created_at => "2023-03-01", :body => "changeset 2 note")
+      comment22 = create(:changeset_comment, :changeset => changeset2, :author => user1, :created_at => "2023-04-01", :body => "changeset 2 extra note")
+      comment23 = create(:changeset_comment, :changeset => changeset2, :author => user2, :created_at => "2023-05-01", :body => "changeset 2 review")
+
+      get api_changeset_comments_path
+      assert_response :success
+      assert_comments_in_order [comment23, comment22, comment21, comment12, comment11]
+
+      get api_changeset_comments_path(:limit => 3)
+      assert_response :success
+      assert_comments_in_order [comment23, comment22, comment21]
+
+      get api_changeset_comments_path(:from => "2023-03-15T00:00:00Z")
+      assert_response :success
+      assert_comments_in_order [comment23, comment22]
+
+      get api_changeset_comments_path(:from => "2023-01-15T00:00:00Z", :to => "2023-04-15T00:00:00Z")
+      assert_response :success
+      assert_comments_in_order [comment22, comment21, comment12]
+
+      get api_changeset_comments_path(:user => user1.id)
+      assert_response :success
+      assert_comments_in_order [comment22, comment21, comment11]
+
+      get api_changeset_comments_path(:from => "2023-03-15T00:00:00Z", :format => "json")
+      assert_response :success
+      js = ActiveSupport::JSON.decode(@response.body)
+      assert_not_nil js
+      assert_equal 2, js["comments"].count
+      assert_equal comment23.id, js["comments"][0]["id"]
+      assert_equal comment22.id, js["comments"][1]["id"]
+    end
+
     def test_create_by_unauthorized
       assert_no_difference "ChangesetComment.count" do
         post changeset_comment_path(create(:changeset, :closed), :text => "This is a comment")
@@ -422,5 +470,20 @@ module Api
       assert_response :success
       assert comment.reload.visible
     end
+
+    private
+
+    ##
+    # check that certain comments exist in the output in the specified order
+    def assert_comments_in_order(comments)
+      assert_dom "osm > comment", comments.size do |dom_comments|
+        comments.zip(dom_comments).each do |comment, dom_comment|
+          assert_dom dom_comment, "> @id", comment.id.to_s
+          assert_dom dom_comment, "> @uid", comment.author.id.to_s
+          assert_dom dom_comment, "> @user", comment.author.display_name
+          assert_dom dom_comment, "> text", comment.body
+        end
+      end
+    end
   end
 end
index 17ceb1b9e5b8b58b96ba2d66d22fe310978716c7..f1a0f766c9b23858c4bf9b8d2e646afeb42bb255 100644 (file)
@@ -1066,6 +1066,37 @@ module Api
       assert_select "gpx", :count => 1 do
         assert_select "wpt", :count => 1
       end
+
+      user2 = create(:user)
+      get search_api_notes_path(:user => user2.id, :format => "xml")
+      assert_response :success
+      assert_equal "application/xml", @response.media_type
+      assert_select "osm", :count => 1 do
+        assert_select "note", :count => 0
+      end
+    end
+
+    def test_search_by_time_success
+      note1 = create(:note, :created_at => "2020-02-01T00:00:00Z", :updated_at => "2020-04-01T00:00:00Z")
+      note2 = create(:note, :created_at => "2020-03-01T00:00:00Z", :updated_at => "2020-05-01T00:00:00Z")
+
+      get search_api_notes_path(:from => "2020-02-15T00:00:00Z", :to => "2020-04-15T00:00:00Z", :format => "xml")
+      assert_response :success
+      assert_equal "application/xml", @response.media_type
+      assert_select "osm", :count => 1 do
+        assert_select "note", :count => 1 do
+          assert_select "id", note2.id.to_s
+        end
+      end
+
+      get search_api_notes_path(:from => "2020-02-15T00:00:00Z", :to => "2020-04-15T00:00:00Z", :sort => "updated_at", :format => "xml")
+      assert_response :success
+      assert_equal "application/xml", @response.media_type
+      assert_select "osm", :count => 1 do
+        assert_select "note", :count => 1 do
+          assert_select "id", note1.id.to_s
+        end
+      end
     end
 
     def test_search_by_bbox_success
index 169338811d3edeaf11a2210fd0b2d1f1989d8570..2705e332d5a5c81b8d3fddafea5a78af5de71d09 100644 (file)
@@ -3,6 +3,10 @@ require "test_helper"
 module Api
   class UserBlocksControllerTest < ActionDispatch::IntegrationTest
     def test_routes
+      assert_routing(
+        { :path => "/api/0.6/user_blocks", :method => :post },
+        { :controller => "api/user_blocks", :action => "create" }
+      )
       assert_routing(
         { :path => "/api/0.6/user_blocks/1", :method => :get },
         { :controller => "api/user_blocks", :action => "show", :id => "1" }
@@ -14,11 +18,22 @@ module Api
     end
 
     def test_show
-      block = create(:user_block)
+      blocked_user = create(:user)
+      creator_user = create(:moderator_user)
+      block = create(:user_block, :user => blocked_user, :creator => creator_user, :reason => "because running tests")
 
       get api_user_block_path(block)
       assert_response :success
-      assert_select "user_block[id='#{block.id}']", 1
+      assert_select "osm>user_block", 1 do
+        assert_select ">@id", block.id.to_s
+        assert_select ">user", 1
+        assert_select ">user>@uid", blocked_user.id.to_s
+        assert_select ">creator", 1
+        assert_select ">creator>@uid", creator_user.id.to_s
+        assert_select ">revoker", 0
+        assert_select ">reason", 1
+        assert_select ">reason", "because running tests"
+      end
 
       get api_user_block_path(block, :format => "json")
       assert_response :success
@@ -32,5 +47,165 @@ module Api
       assert_response :not_found
       assert_equal "text/plain", @response.media_type
     end
+
+    def test_create_no_permission
+      blocked_user = create(:user)
+      assert_empty blocked_user.blocks
+
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1)
+      assert_response :unauthorized
+      assert_empty blocked_user.blocks
+
+      regular_creator_user = create(:user)
+      auth_header = bearer_authorization_header(regular_creator_user, :scopes => %w[read_prefs])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :forbidden
+      assert_empty blocked_user.blocks
+
+      auth_header = bearer_authorization_header(regular_creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :forbidden
+      assert_empty blocked_user.blocks
+
+      moderator_creator_user = create(:moderator_user)
+      auth_header = bearer_authorization_header(moderator_creator_user, :scopes => %w[read_prefs])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :forbidden
+      assert_empty blocked_user.blocks
+    end
+
+    def test_create_invalid_because_no_user
+      blocked_user = create(:user, :deleted)
+      assert_empty blocked_user.blocks
+
+      creator_user = create(:moderator_user)
+      auth_header = bearer_authorization_header(creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:reason => "because", :period => 1), :headers => auth_header
+      assert_response :bad_request
+      assert_equal "text/plain", @response.media_type
+      assert_equal "No user was given", @response.body
+
+      assert_empty blocked_user.blocks
+    end
+
+    def test_create_invalid_because_user_is_unknown
+      creator_user = create(:moderator_user)
+      auth_header = bearer_authorization_header(creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:user => 0, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :not_found
+      assert_equal "text/plain", @response.media_type
+    end
+
+    def test_create_invalid_because_user_is_deleted
+      blocked_user = create(:user, :deleted)
+      assert_empty blocked_user.blocks
+
+      creator_user = create(:moderator_user)
+      auth_header = bearer_authorization_header(creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :not_found
+      assert_equal "text/plain", @response.media_type
+
+      assert_empty blocked_user.blocks
+    end
+
+    def test_create_invalid_because_missing_reason
+      create_with_params_and_assert_bad_request("No reason was given", :period => "10")
+    end
+
+    def test_create_invalid_because_missing_period
+      create_with_params_and_assert_bad_request("No period was given", :reason => "because")
+    end
+
+    def test_create_invalid_because_non_numeric_period
+      create_with_params_and_assert_bad_request("Period should be a number of hours", :reason => "because", :period => "one hour")
+    end
+
+    def test_create_invalid_because_negative_period
+      create_with_params_and_assert_bad_request("Period must be between 0 and #{UserBlock::PERIODS.max}", :reason => "go away", :period => "-1")
+    end
+
+    def test_create_invalid_because_excessive_period
+      create_with_params_and_assert_bad_request("Period must be between 0 and #{UserBlock::PERIODS.max}", :reason => "go away", :period => "10000000")
+    end
+
+    def test_create_invalid_because_unknown_needs_view
+      create_with_params_and_assert_bad_request("Needs_view must be true if provided", :reason => "because", :period => "1", :needs_view => "maybe")
+    end
+
+    def test_create_success
+      blocked_user = create(:user)
+      creator_user = create(:moderator_user)
+
+      assert_empty blocked_user.blocks
+      auth_header = bearer_authorization_header(creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => 1), :headers => auth_header
+      assert_response :success
+      assert_equal 1, blocked_user.blocks.length
+
+      block = blocked_user.blocks.take
+      assert_predicate block, :active?
+      assert_equal "because", block.reason
+      assert_equal creator_user, block.creator
+
+      assert_equal "application/xml", @response.media_type
+      assert_select "osm>user_block", 1 do
+        assert_select ">@id", block.id.to_s
+        assert_select ">@needs_view", "false"
+        assert_select ">user", 1
+        assert_select ">user>@uid", blocked_user.id.to_s
+        assert_select ">creator", 1
+        assert_select ">creator>@uid", creator_user.id.to_s
+        assert_select ">revoker", 0
+        assert_select ">reason", 1
+        assert_select ">reason", "because"
+      end
+    end
+
+    def test_create_success_with_needs_view
+      blocked_user = create(:user)
+      creator_user = create(:moderator_user)
+
+      assert_empty blocked_user.blocks
+      auth_header = bearer_authorization_header(creator_user, :scopes => %w[read_prefs write_blocks])
+      post api_user_blocks_path(:user => blocked_user.id, :reason => "because", :period => "1", :needs_view => "true"), :headers => auth_header
+      assert_response :success
+      assert_equal 1, blocked_user.blocks.length
+
+      block = blocked_user.blocks.take
+      assert_predicate block, :active?
+      assert_equal "because", block.reason
+      assert_equal creator_user, block.creator
+
+      assert_equal "application/xml", @response.media_type
+      assert_select "osm>user_block", 1 do
+        assert_select ">@id", block.id.to_s
+        assert_select ">@needs_view", "true"
+        assert_select ">user", 1
+        assert_select ">user>@uid", blocked_user.id.to_s
+        assert_select ">creator", 1
+        assert_select ">creator>@uid", creator_user.id.to_s
+        assert_select ">revoker", 0
+        assert_select ">reason", 1
+        assert_select ">reason", "because"
+      end
+    end
+
+    private
+
+    def create_with_params_and_assert_bad_request(message, **params)
+      blocked_user = create(:user)
+      assert_empty blocked_user.blocks
+
+      moderator_creator_user = create(:moderator_user)
+      auth_header = bearer_authorization_header(moderator_creator_user, :scopes => %w[read_prefs write_blocks])
+
+      post api_user_blocks_path({ :user => blocked_user.id }.merge(params)), :headers => auth_header
+      assert_response :bad_request
+      assert_equal "text/plain", @response.media_type
+      assert_equal message, @response.body
+
+      assert_empty blocked_user.blocks
+    end
   end
 end
diff --git a/test/system/account_home_test.rb b/test/system/account_home_test.rb
new file mode 100644 (file)
index 0000000..813c45e
--- /dev/null
@@ -0,0 +1,57 @@
+require "application_system_test_case"
+
+class AccountHomeTest < ApplicationSystemTestCase
+  test "Go to Home Location works on map layout pages" do
+    user = create(:user, :display_name => "test user", :home_lat => 60, :home_lon => 30)
+    sign_in_as(user)
+
+    visit root_path
+    assert_no_selector "img.leaflet-marker-icon"
+
+    click_on "test user"
+    click_on "Go to Home Location"
+    all "img.leaflet-marker-icon", :count => 1 do |marker|
+      assert_equal "My home location", marker["title"]
+    end
+
+    click_on "OpenStreetMap logo"
+    assert_no_selector "img.leaflet-marker-icon"
+  end
+
+  test "Go to Home Location works on non-map layout pages" do
+    user = create(:user, :display_name => "test user", :home_lat => 60, :home_lon => 30)
+    sign_in_as(user)
+
+    visit about_path
+    assert_no_selector "img.leaflet-marker-icon"
+
+    click_on "test user"
+    click_on "Go to Home Location"
+    all "img.leaflet-marker-icon", :count => 1 do |marker|
+      assert_equal "My home location", marker["title"]
+    end
+
+    click_on "OpenStreetMap logo"
+    assert_no_selector "img.leaflet-marker-icon"
+  end
+
+  test "Go to Home Location is not available for users without home location" do
+    user = create(:user, :display_name => "test user")
+    sign_in_as(user)
+
+    visit root_path
+    assert_no_selector "img.leaflet-marker-icon"
+
+    click_on "test user"
+    assert_no_link "Go to Home Location"
+  end
+
+  test "account home page shows a warning when visited by users without home location" do
+    user = create(:user, :display_name => "test user")
+    sign_in_as(user)
+
+    visit account_home_path
+    assert_no_selector "img.leaflet-marker-icon"
+    assert_text "Home location is not set"
+  end
+end
index e26ae89ac6556cbec289f25e52ed445ac98e33b6..ae5e114c3d19d4a415633717fa606921a797cfa1 100644 (file)
@@ -5,7 +5,10 @@ class IssuesTest < ApplicationSystemTestCase
 
   def test_view_issues_not_logged_in
     visit issues_path
-    assert_content "Log in"
+
+    within_content_heading do
+      assert_content "Log In"
+    end
   end
 
   def test_view_issues_normal_user
index 2fb90fc3a41acf81873d53ab3d640bc9b34e1771..2d05447a6a280980128e605e4d4b16aee4b4eb59 100644 (file)
@@ -79,7 +79,9 @@ class UserSignupTest < ApplicationSystemTestCase
   test "Sign up from login page" do
     visit login_path
 
-    click_on "Sign up"
+    within_content_heading do
+      click_on "Sign Up"
+    end
 
     within_content_body do
       assert_content "Confirm Password"
index a7251235770971ff33b49f2bcb3cc912f604b3f5..d6368b68b28edfb4a8aa825428341d93b660dcea 100644 (file)
@@ -5,7 +5,7 @@ class UserSuspensionTest < ApplicationSystemTestCase
     user = create(:user)
     sign_in_as(user)
     visit edit_account_path
-    assert_content "My Settings"
+    assert_content "My Account"
 
     user.suspend!
 
index 8a9dc001f3150b43cb9c671a724ffec855779198..a64274c0a6fa5a5c79008af8393c187f73cc1397 100644 (file)
@@ -100,10 +100,13 @@ Teaspoon.configure do |config|
   # Capybara Webkit: https://github.com/jejacks0n/teaspoon/wiki/Using-Capybara-Webkit
   require "selenium-webdriver"
   config.driver = :selenium
+  firefox_options = Selenium::WebDriver::Firefox::Options.new
+  firefox_options.args = ["-headless"] if Settings.system_test_headless
+  firefox_options.binary = Settings.system_test_firefox_binary if Settings.system_test_firefox_binary
   config.driver_options = {
     :client_driver => :firefox,
     :selenium_options => {
-      :options => Selenium::WebDriver::Firefox::Options.new(:args => ["-headless"])
+      :options => firefox_options
     }
   }