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
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]
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
//= require index/directions
//= require index/changeset
//= require index/query
+//= require index/home
//= require router
$(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();
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,
"/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") {
--- /dev/null
+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;
+};
-.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;
+ }
+ }
}
--- /dev/null
+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
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
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
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]
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"])
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)
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
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
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]
@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|
# 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|
@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]
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"
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|
# 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
module Api
class UserBlocksController < ApiController
+ before_action :check_api_writable, :only => :create
+ before_action :authorize, :only => :create
+
authorize_resource
before_action :set_request_formats
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
module ChangesetComments
class FeedsController < ApplicationController
+ include QueryMethods
+
before_action :authorize_web
before_action :set_locale
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
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
--- /dev/null
+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
@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
<% end %>
<% content_for :heading do %>
- <h1><%= t ".my settings" %></h1>
+ <h1><%= t ".my_account" %></h1>
<% end %>
<%= render :partial => "settings_menu" %>
--- /dev/null
+<% content_for(:content_class) { "overlay-sidebar" } %>
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+json.partial! "api/root_attributes"
+
+json.comments(@comments) do |comment|
+ json.partial! comment
+end
--- /dev/null
+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
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
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
<%= 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}" %>
<% 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 %>
<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>
+<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 -%>
<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
+<html lang="<%= I18n.locale %>" dir="<%= dir %>">
<head>
<meta charset="utf-8">
<title>OpenStreetMap</title>
<%= 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>
<% 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 %>
<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>
<% 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",
<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" %>
<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? %>
accounts:
edit:
title: "Edit account"
- my settings: My Settings
+ my_account: My Account
current email address: "Current Email Address"
external auth: "External Authentication"
openid:
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"
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"
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
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.'
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
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}"
index:
heading_html: "%{user}'s Comments"
changesets: "Changesets"
- diary_entries: "Diary entries"
+ diary_entries: "Diary Entries"
no_comments: "No comments"
changeset_comments:
index:
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"
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
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
resource :terms, :only => [:show, :update]
resource :pd_declaration, :only => [:show, :create]
resource :deletion, :only => :show
+ resource :home, :only => :show
end
end
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)
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:
point.altitude ||= 0
yield point
@actual_points += 1
+ @lats << point.latitude
+ @lons << point.longitude
elsif reader.name == "trkseg"
@tracksegs += 1
end
@possible_points = 0
@actual_points = 0
@tracksegs = 0
+ @lats = []
+ @lons = []
begin
Archive::Reader.open_filename(@file).each_entry_with_data do |entry, data|
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
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
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
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
def within_content_body(&)
within("#content > .content-body", &)
end
+
+ def within_content_heading(&)
+ within("#content > .content-heading", &)
+ end
end
##
# 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" }
)
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")
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
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
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" }
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
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
--- /dev/null
+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
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
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"
user = create(:user)
sign_in_as(user)
visit edit_account_path
- assert_content "My Settings"
+ assert_content "My Account"
user.suspend!
# 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
}
}