]> git.openstreetmap.org Git - rails.git/commitdiff
Merge pull request #5677 from tomhughes/user-list-enhancements
authorAndy Allan <git@gravitystorm.co.uk>
Sat, 15 Feb 2025 17:37:48 +0000 (17:37 +0000)
committerGitHub <noreply@github.com>
Sat, 15 Feb 2025 17:37:48 +0000 (17:37 +0000)
User list enhancements

44 files changed:
app/abilities/ability.rb
app/abilities/api_ability.rb
app/assets/javascripts/application.js
app/assets/javascripts/index.js
app/assets/javascripts/index/changeset.js
app/assets/javascripts/index/home.js [new file with mode: 0644]
app/assets/javascripts/index/note.js
app/assets/javascripts/leaflet.map.js
app/assets/javascripts/leaflet.sidebar.js
app/assets/stylesheets/common.scss
app/assets/stylesheets/errors.scss
app/controllers/accounts/homes_controller.rb [new file with mode: 0644]
app/controllers/accounts/pd_declarations_controller.rb [new file with mode: 0644]
app/controllers/accounts/terms_controller.rb
app/controllers/api/user_blocks_controller.rb
app/controllers/users_controller.rb
app/helpers/application_helper.rb
app/views/accounts/edit.html.erb
app/views/accounts/homes/show.html.erb [new file with mode: 0644]
app/views/accounts/pd_declarations/show.html.erb [new file with mode: 0644]
app/views/accounts/terms/show.html.erb
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/new.html.erb
app/views/users/show.html.erb
config/locales/en.yml
config/routes.rb
config/settings/test.yml
lib/gpx.rb
lib/oauth.rb
test/application_system_test_case.rb
test/controllers/accounts/pd_declarations_controller_test.rb [new file with mode: 0644]
test/controllers/accounts/terms_controller_test.rb
test/controllers/api/user_blocks_controller_test.rb
test/integration/user_creation_test.rb
test/system/account_home_test.rb [new file with mode: 0644]
test/system/account_pd_declaration_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 3116bc5cdd451501ccbaebe66d3034f0178c2887..dd377a727c817d275fa97d5339aef32904c178ab 100644 (file)
@@ -29,7 +29,7 @@ class Ability
 
     if user&.active?
       can :welcome, :site
-      can :read, [:deletion, :account_terms]
+      can :read, [:deletion, :account_terms, :account_pd_declaration, :account_home]
 
       if Settings.status != "database_offline"
         can [:read, :create, :destroy], :changeset_subscription
@@ -38,6 +38,7 @@ class Ability
         can [:read, :create, :destroy], :oauth2_authorization
         can [:update, :destroy], :account
         can :update, :account_terms
+        can :create, :account_pd_declaration
         can :read, :dashboard
         can [:create, :subscribe, :unsubscribe], DiaryEntry
         can :update, DiaryEntry, :user => user
index 7bbd9889ad53fbbb6d064b115f79eca7a54cad75..ec4de8f8e03b99dc795f88cecf6421aefb11c00e 100644 (file)
@@ -43,7 +43,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 2e13f69671bec7253adbe5c98887a33c105d7725..464bbecee17db7f7ded5ddcf115ff0dc2c13395b 100644 (file)
@@ -90,21 +90,76 @@ $(document).ready(function () {
   // See https://turbo.hotwired.dev/reference/drive#turbo.session.drive
   Turbo.session.drive = false;
 
-  var headerWidth = 0,
-      compactWidth = 0;
+  const $expandedSecondaryMenu = $("header nav.secondary > ul"),
+        $collapsedSecondaryMenu = $("#compact-secondary-nav > ul"),
+        secondaryMenuItems = [],
+        breakpointWidth = 768;
+  let moreItemWidth = 0;
 
   function updateHeader() {
     var windowWidth = $(window).width();
 
-    if (windowWidth < compactWidth) {
-      $("body").removeClass("compact-nav").addClass("small-nav");
-    } else if (windowWidth < headerWidth) {
-      $("body").addClass("compact-nav").removeClass("small-nav");
+    if (windowWidth < breakpointWidth) {
+      $("body").addClass("small-nav");
+      expandAllSecondaryMenuItems();
     } else {
-      $("body").removeClass("compact-nav").removeClass("small-nav");
+      $("body").removeClass("small-nav");
+      const availableWidth = $expandedSecondaryMenu.width();
+      secondaryMenuItems.forEach(function (item) {
+        $(item[0]).remove();
+      });
+      let runningWidth = 0,
+          i = 0,
+          requiredWidth;
+      for (; i < secondaryMenuItems.length; i++) {
+        runningWidth += secondaryMenuItems[i][1];
+        if (i < secondaryMenuItems.length - 1) {
+          requiredWidth = runningWidth + moreItemWidth;
+        } else {
+          requiredWidth = runningWidth;
+        }
+        if (requiredWidth > availableWidth) {
+          break;
+        }
+        expandSecondaryMenuItem($(secondaryMenuItems[i][0]));
+      }
+      for (; i < secondaryMenuItems.length; i++) {
+        collapseSecondaryMenuItem($(secondaryMenuItems[i][0]));
+      }
     }
   }
 
+  function expandAllSecondaryMenuItems() {
+    secondaryMenuItems.forEach(function (item) {
+      expandSecondaryMenuItem($(item[0]));
+    });
+  }
+
+  function expandSecondaryMenuItem($item) {
+    $item.children("a")
+      .removeClass("dropdown-item")
+      .addClass("nav-link")
+      .addClass(function () {
+        return $(this).hasClass("active") ? "text-secondary-emphasis" : "text-secondary";
+      });
+    $item.addClass("nav-item").insertBefore("#compact-secondary-nav");
+    toggleCompactSecondaryNav();
+  }
+
+  function collapseSecondaryMenuItem($item) {
+    $item.children("a")
+      .addClass("dropdown-item")
+      .removeClass("nav-link text-secondary text-secondary-emphasis");
+    $item.removeClass("nav-item").appendTo($collapsedSecondaryMenu);
+    toggleCompactSecondaryNav();
+  }
+
+  function toggleCompactSecondaryNav() {
+    $("#compact-secondary-nav").toggle(
+      $collapsedSecondaryMenu.find("li").length > 0
+    );
+  }
+
   /*
    * Chrome 60 and later seem to fire the "ready" callback
    * before the DOM is fully ready causing us to measure the
@@ -112,20 +167,10 @@ $(document).ready(function () {
    * to defer the measurement slightly as a workaround.
    */
   setTimeout(function () {
-    $("header").children(":visible").each(function (i, e) {
-      headerWidth += $(e).outerWidth();
+    $expandedSecondaryMenu.find("li:not(#compact-secondary-nav)").each(function () {
+      secondaryMenuItems.push([this, $(this).width()]);
     });
-
-    $("body").addClass("compact-nav");
-
-    $("header").children(":visible").each(function (i, e) {
-      compactWidth += $(e).outerWidth();
-    });
-
-    $("body").removeClass("compact-nav");
-
-    $("header").removeClass("text-nowrap");
-    $("header nav.secondary > ul").removeClass("flex-nowrap");
+    moreItemWidth = $("#compact-secondary-nav").width();
 
     updateHeader();
 
index c3ee1e3bc9b47729faab64ad478d6a7818cbabb4..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,
@@ -309,8 +308,9 @@ $(document).ready(function () {
     };
 
     function addObject(type, id, center) {
+      var hashParams = OSM.parseHash(window.location.hash);
       map.addObject({ type: type, id: parseInt(id, 10) }, function (bounds) {
-        if (!window.location.hash && bounds.isValid() &&
+        if (!hashParams.center && bounds.isValid() &&
             (center || !map.getBounds().contains(bounds))) {
           OSM.router.withoutMoveListener(function () {
             map.fitBounds(bounds);
@@ -356,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") {
index 39b4abde15bd7fc836e641733bfec9af1dd8ee14..d3e61270bf7dac9945d0c3a84457d821f39d3b82 100644 (file)
@@ -12,9 +12,10 @@ OSM.Changeset = function (map) {
     const changesetData = content.find("[data-changeset]").data("changeset");
     changesetData.type = "changeset";
 
+    var hashParams = OSM.parseHash(window.location.hash);
     initialize();
     map.addObject(changesetData, function (bounds) {
-      if (!window.location.hash && bounds.isValid()) {
+      if (!hashParams.center && bounds.isValid()) {
         OSM.router.withoutMoveListener(function () {
           map.fitBounds(bounds);
         });
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 e9c51f9bf1df571f6814be4999a10517975fd28b..6a0487aaac21a2622386f66257860975357b833d 100644 (file)
@@ -27,13 +27,16 @@ OSM.Note = function (map) {
       var data = $(".details").data();
       if (!data) return;
       var latLng = L.latLng(data.coordinates.split(","));
-      if (!map.getBounds().contains(latLng)) moveToNote();
+      if (!map.getBounds().contains(latLng)) {
+        OSM.router.withoutMoveListener(function () {
+          map.setView(latLng, 15, { reset: true });
+        });
+      }
     });
   };
 
   page.load = function (path, id) {
     initialize(path, id);
-    moveToNote();
   };
 
   function initialize(path, id) {
@@ -48,7 +51,6 @@ OSM.Note = function (map) {
         success: () => {
           OSM.loadSidebarContent(path, () => {
             initialize(path, id);
-            moveToNote();
           });
         },
         error: (xhr) => {
@@ -77,11 +79,19 @@ OSM.Note = function (map) {
     var data = $(".details").data();
 
     if (data) {
+      var hashParams = OSM.parseHash(window.location.hash);
       map.addObject({
         type: "note",
         id: parseInt(id, 10),
         latLng: L.latLng(data.coordinates.split(",")),
         icon: noteIcons[data.status]
+      }, function () {
+        if (!hashParams.center) {
+          var latLng = L.latLng(data.coordinates.split(","));
+          OSM.router.withoutMoveListener(function () {
+            map.setView(latLng, 15, { reset: true });
+          });
+        }
       });
     }
   }
@@ -99,18 +109,6 @@ OSM.Note = function (map) {
     }
   }
 
-  function moveToNote() {
-    var data = $(".details").data();
-    if (!data) return;
-    var latLng = L.latLng(data.coordinates.split(","));
-
-    if (!window.location.hash || window.location.hash.match(/^#?c[0-9]+$/)) {
-      OSM.router.withoutMoveListener(function () {
-        map.setView(latLng, 15, { reset: true });
-      });
-    }
-  }
-
   page.unload = function () {
     map.removeObject();
   };
index b101017219c12b64f19e522005fa19bdd291e4ba..1b92edf9945ae7f7b996f9cff7a454b1254979d7 100644 (file)
@@ -347,15 +347,22 @@ L.OSM.Map = L.Map.extend({
   },
 
   setSidebarOverlaid: function (overlaid) {
-    var sidebarWidth = 350;
+    var mediumDeviceWidth = window.getComputedStyle(document.documentElement).getPropertyValue("--bs-breakpoint-md");
+    var isMediumDevice = window.matchMedia(`(max-width: ${mediumDeviceWidth})`).matches;
+    var sidebarWidth = $("#sidebar").width();
+    var sidebarHeight = $("#sidebar").height();
     if (overlaid && !$("#content").hasClass("overlay-sidebar")) {
       $("#content").addClass("overlay-sidebar");
       this.invalidateSize({ pan: false });
-      if ($("html").attr("dir") !== "rtl") {
+      if (isMediumDevice) {
+        this.panBy([0, -sidebarHeight], { animate: false });
+      } else if ($("html").attr("dir") !== "rtl") {
         this.panBy([-sidebarWidth, 0], { animate: false });
       }
     } else if (!overlaid && $("#content").hasClass("overlay-sidebar")) {
-      if ($("html").attr("dir") !== "rtl") {
+      if (isMediumDevice) {
+        this.panBy([0, $("#map").height() / 2], { animate: false });
+      } else if ($("html").attr("dir") !== "rtl") {
         this.panBy([sidebarWidth, 0], { animate: false });
       }
       $("#content").removeClass("overlay-sidebar");
index 9573c839d411c4a6eb5be8ebaed9d84a186cf8af..1c911a961856e4cfae4cf63816c196ef31bf877e 100644 (file)
@@ -17,6 +17,8 @@ L.OSM.sidebar = function (selector) {
   };
 
   control.togglePane = function (pane, button) {
+    var mediumDeviceWidth = window.getComputedStyle(document.documentElement).getPropertyValue("--bs-breakpoint-md");
+    var isMediumDevice = window.matchMedia(`(max-width: ${mediumDeviceWidth})`).matches;
     var paneWidth = 250;
 
     current
@@ -27,18 +29,22 @@ L.OSM.sidebar = function (selector) {
       .removeClass("active");
 
     if (current === pane) {
-      if ($("html").attr("dir") === "rtl") {
-        map.panBy([-paneWidth, 0], { animate: false });
-      }
       $(sidebar).hide();
       $("#content").addClass("overlay-right-sidebar");
       current = currentButton = $();
+      if (isMediumDevice) {
+        map.panBy([0, -$("#map").height() / 2], { animate: false });
+      } else if ($("html").attr("dir") === "rtl") {
+        map.panBy([-paneWidth, 0], { animate: false });
+      }
     } else {
       $(sidebar).show();
       $("#content").removeClass("overlay-right-sidebar");
       current = pane;
       currentButton = button || $();
-      if ($("html").attr("dir") === "rtl") {
+      if (isMediumDevice) {
+        map.panBy([0, $("#map").height()], { animate: false });
+      } else if ($("html").attr("dir") === "rtl") {
         map.panBy([paneWidth, 0], { animate: false });
       }
     }
index fc9af9803e53cbcdb340680b11ba95e9be3769e4..a27976854f99dc90af33a7878e326ccdff9413f8 100644 (file)
@@ -131,10 +131,6 @@ header {
     font-size: 14px;
   }
 
-  nav.primary {
-    margin-right: auto;
-  }
-
   .username {
     max-width: 12em;
   }
@@ -174,7 +170,11 @@ nav.primary {
 
 nav.secondary {
   .nav-link {
-    padding: 0.3rem;
+    padding: 0 0.3rem;
+  }
+
+  > ul {
+    height: 1.5em;
   }
 }
 
@@ -191,15 +191,6 @@ nav.primary, nav.secondary {
   display: none;
 }
 
-body.compact-nav {
-  #compact-secondary-nav {
-    display: inline-block;
-  }
-  .compact-hide {
-    display: none;
-  }
-}
-
 body.small-nav {
   #menu-icon {
     display: block;
@@ -240,6 +231,10 @@ body.small-nav {
   nav.secondary {
     flex-direction: column;
 
+    > ul {
+      height: auto;
+    }
+
     .user-menu, .login-menu {
       width: 100%;
     }
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
diff --git a/app/controllers/accounts/pd_declarations_controller.rb b/app/controllers/accounts/pd_declarations_controller.rb
new file mode 100644 (file)
index 0000000..2d2569d
--- /dev/null
@@ -0,0 +1,28 @@
+module Accounts
+  class PdDeclarationsController < ApplicationController
+    layout "site"
+
+    before_action :authorize_web
+    before_action :set_locale
+
+    authorize_resource :class => :account_pd_declaration
+
+    def show; end
+
+    def create
+      if current_user.consider_pd
+        flash[:warning] = t(".already_declared")
+      else
+        current_user.consider_pd = params[:consider_pd]
+
+        if current_user.consider_pd
+          flash[:notice] = t(".successfully_declared") if current_user.save
+        else
+          flash[:warning] = t(".did_not_confirm")
+        end
+      end
+
+      redirect_to edit_account_path
+    end
+  end
+end
index 13e9de890925c4704d93b71b4701fcf52f144c72..03007a5323f6788152d37eeb43eff5aa40a9dde0 100644 (file)
@@ -33,7 +33,6 @@ module Accounts
         flash[:notice] = { :partial => "accounts/terms/terms_declined_flash" } if current_user.save
       else
         unless current_user.terms_agreed?
-          current_user.consider_pd = params[:user][:consider_pd]
           current_user.tou_agreed = Time.now.utc
           current_user.terms_agreed = Time.now.utc
           current_user.terms_seen = true
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 a0be87bdc11fa6417601b6e278030fc673825515..0df971bd4f39dd55d2f1eba2307143a41f2aae5a 100644 (file)
@@ -222,8 +222,7 @@ class UsersController < ApplicationController
   def user_params
     params.require(:user).permit(:email, :display_name,
                                  :auth_provider, :auth_uid,
-                                 :pass_crypt, :pass_crypt_confirmation,
-                                 :consider_pd)
+                                 :pass_crypt, :pass_crypt_confirmation)
   end
 
   ##
index ff6dcd2ff194820556e8a85382f97677ddd97d30..86a3d56cdb96c2df8d8cf6c45e71a839c5a165c6 100644 (file)
@@ -43,7 +43,7 @@ module ApplicationHelper
   end
 
   def header_nav_link_class(path)
-    ["nav-link", current_page?(path) ? "text-secondary-emphasis" : "text-secondary"]
+    ["nav-link", current_page?(path) ? "active text-secondary-emphasis" : "text-secondary"]
   end
 
   def application_data
index 7a10b12e3d281c181248d9bd5b11312eea62c7b0..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" %>
     <small class="form-text text-body-secondary">(<a href="<%= t ".openid.link" %>" target="_new"><%= t ".openid.link text" %></a>)</small>
   </fieldset>
 
-  <div class="mb-3">
-    <label class="form-label"><%= t ".contributor terms.heading" %></label>
+  <div class="mb-3 d-flex flex-column flex-sm-row column-gap-1">
+    <label class="form-label text-nowrap mb-0"><%= t ".contributor terms.heading" %></label>
     <span class="form-text text-body-secondary">
       <% if current_user.terms_agreed? %>
         <%= t ".contributor terms.agreed" %>
         (<a href="<%= t ".contributor terms.link" %>" target="_new"><%= t ".contributor terms.link text" %></a>)
+        <br>
         <% if current_user.consider_pd? %>
           <%= t ".contributor terms.agreed_with_pd" %>
+        <% else %>
+          <%= t ".contributor terms.not_agreed_with_pd" %>
+          (<%= link_to t(".contributor terms.pd_link_text"), account_pd_declaration_path %>)
         <% end %>
       <% else %>
         <%= t ".contributor terms.not yet agreed" %>
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/accounts/pd_declarations/show.html.erb b/app/views/accounts/pd_declarations/show.html.erb
new file mode 100644 (file)
index 0000000..ad314fe
--- /dev/null
@@ -0,0 +1,14 @@
+<% content_for :heading do %>
+  <h1><%= t ".title" %></h1>
+<% end %>
+
+<%= bootstrap_form_tag do |f| %>
+  <%= f.form_group :help => link_to(t(".consider_pd_why"), t(".consider_pd_why_url"), :target => :new) do %>
+    <%= f.check_box :consider_pd,
+                    :label => t(".consider_pd"),
+                    :autocomplete => :off,
+                    :checked => current_user.consider_pd,
+                    :disabled => current_user.consider_pd %>
+  <% end %>
+  <%= f.primary t(".confirm"), :disabled => current_user.consider_pd %>
+<% end %>
index 3cc52302ff8a3d537d958e4ecf65ea5ecc19a6d6..c1c0e0a89a77760da67d05d65de7ade49d9074c7 100644 (file)
     <%= submit_tag(t(".continue"), :name => "continue", :id => "continue", :disabled => true, :class => "btn btn-primary") %>
     <%= submit_tag(t(".cancel"), :name => "decline", :id => "decline", :class => "btn btn-outline-secondary") %>
   </div>
-
-  <div class="mb-3">
-    <div class="form-check">
-      <%= check_box("user", "consider_pd", :class => "form-check-input") %>
-    <label for="user_consider_pd" class="form-check-label">
-      <%= t ".consider_pd" %>
-    </label>
-    <span class="minorNote">(<%= link_to(t(".consider_pd_why"), t(".consider_pd_why_url"), :target => :new) %>)</span>
-  </div>
 <% 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 45e23fc2b914471be7f4f40a830ef341447c2ef6..f09812972ef31ff234233072891f20a0bb1d4300 100644 (file)
       </ul>
     </div>
   </nav>
-  <nav class='secondary d-flex gap-2 align-items-center'>
-    <ul class='nav flex-nowrap'>
+  <nav class='secondary d-flex gap-2 flex-grow-1 align-items-center'>
+    <ul id='secondary-nav-menu' class='nav flex-grow-1' data-turbo-permanent>
       <% if Settings.status != "database_offline" && can?(:index, Issue) %>
-        <li class="compact-hide nav-item">
+        <li class="nav-item">
           <%= link_to issues_path(:status => "open"), :class => header_nav_link_class(issues_path) do %>
             <%= t("layouts.issues") %>
             <%= open_issues_count %>
           <% end -%>
         </li>
       <% end %>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.history"), history_path, :class => header_nav_link_class(history_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.export"), export_path, :class => header_nav_link_class(export_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.gps_traces"), traces_path, :class => header_nav_link_class(traces_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.user_diaries"), diary_entries_path, :class => header_nav_link_class(diary_entries_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.communities"), communities_path, :class => header_nav_link_class(communities_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.copyright"), copyright_path, :class => header_nav_link_class(copyright_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.help"), help_path, :class => header_nav_link_class(help_path) %>
       </li>
-      <li class="compact-hide nav-item">
+      <li class="nav-item">
         <%= link_to t("layouts.about"), about_path, :class => header_nav_link_class(about_path) %>
       </li>
-      <li id="compact-secondary-nav" class="dropdown nav-item">
+      <li id="compact-secondary-nav" class="dropdown nav-item ms-auto">
         <button class="dropdown-toggle nav-link btn btn-outline-secondary border-0 bg-body text-secondary" type="button" data-bs-toggle="dropdown"><%= t "layouts.more" %></button>
         <ul class="dropdown-menu">
-          <% if Settings.status != "database_offline" && can?(:index, Issue) %>
-            <li>
-              <%= link_to issues_path(:status => "open"), :class => "dropdown-item" do %>
-                <%= t("layouts.issues") %>
-                <%= open_issues_count %>
-              <% end -%>
-            </li>
-          <% end %>
-          <li><%= link_to t("layouts.history"), history_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.export"), export_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.gps_traces"), traces_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.user_diaries"), diary_entries_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.communities"), communities_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.copyright"), copyright_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.help"), help_path, :class => "dropdown-item" %></li>
-          <li><%= link_to t("layouts.about"), about_path, :class => "dropdown-item" %></li>
         </ul>
       </li>
     </ul>
             <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 37493418ab0836814c9e3ea0aa38d7c805a1b5a5..22db279d9ebe6723580dbe9817511d068573292b 100644 (file)
                                                  :contributor_terms_link => link_to(t(".by_signing_up.contributor_terms"),
                                                                                     t(".by_signing_up.contributor_terms_url"),
                                                                                     :target => :new)) %></p>
-  <%= f.form_group do %>
-    <%= f.check_box :consider_pd,
-                    :tabindex => 5,
-                    :label => t(".consider_pd_html",
-                                :consider_pd_link => link_to(t(".consider_pd"),
-                                                             t(".consider_pd_url"),
-                                                             :target => :new)) %>
-  <% end %>
 
   <div class="mb-3">
-    <%= submit_tag(t(".continue"), :name => "continue", :id => "continue", :class => "btn btn-primary", :tabindex => 6) %>
+    <%= submit_tag(t(".continue"), :name => "continue", :id => "continue", :class => "btn btn-primary", :tabindex => 5) %>
   </div>
 <% end %>
 
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 03458b00d566626d6e0c25b894de1fbeabbce70c..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:
@@ -260,6 +260,8 @@ en:
         agreed_with_pd: "You have also declared that you consider your edits to be in the Public Domain."
         link: "https://osmfoundation.org/wiki/Licence/Contributor_Terms"
         link text: "what is this?"
+        not_agreed_with_pd: "You haven't declared that you consider your edits to be in the Public Domain."
+        pd_link_text: "declare"
       save changes button: Save Changes
       delete_account: Delete Account...
     go_public:
@@ -305,9 +307,6 @@ en:
         read_ct: "I have read and agree to the above contributor terms"
         tou_explain_html: "These %{tou_link} govern the use of the website and other infrastructure provided by the OSMF. Please click on the link, read and agree to the text."
         read_tou: "I have read and agree to the Terms of Use"
-        consider_pd: "In addition to the above, I consider my contributions to be in the Public Domain"
-        consider_pd_why: "what's this?"
-        consider_pd_why_url: https://osmfoundation.org/wiki/Licence_and_Legal_FAQ/Why_would_I_want_my_contributions_to_be_public_domain
         guidance_info_html: "Information to help understand these terms: a %{readable_summary_link} and some %{informal_translations_link}"
         readable_summary: human readable summary
         informal_translations: informal translations
@@ -325,6 +324,17 @@ en:
         terms_declined_html: We are sorry that you have decided to not accept the new Contributor Terms. For more information, please see %{terms_declined_link}.
         terms_declined_link: this wiki page
         terms_declined_url: https://wiki.openstreetmap.org/wiki/Contributor_Terms_Declined
+    pd_declarations:
+      show:
+        title: Consider my contributions to be in the Public Domain
+        consider_pd: "I consider my contributions to be in the Public Domain"
+        consider_pd_why: "Why would I want my contributions to be Public Domain?"
+        consider_pd_why_url: https://osmfoundation.org/wiki/Licence_and_Legal_FAQ/Why_would_I_want_my_contributions_to_be_public_domain
+        confirm: Confirm
+      create:
+        successfully_declared: "You have successfully declared that you consider your edits to be in the Public Domain."
+        already_declared: "You have already declared that you consider your edits to be in the Public Domain."
+        did_not_confirm: "You didn't confirm that you consider your edits to be in the Public Domain."
   browse:
     deleted_ago_by_html: "Deleted %{time_ago} by %{user}"
     edited_ago_by_html: "Edited %{time_ago} by %{user}"
@@ -1912,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"
@@ -2643,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"
@@ -2703,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
@@ -2760,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.'
@@ -2785,9 +2796,6 @@ en:
         privacy_policy_url: https://osmfoundation.org/wiki/Privacy_Policy
         privacy_policy_title: OSMF privacy policy including section on email addresses
         html: 'Your address is not displayed publicly, see our %{privacy_policy_link} for more information.'
-      consider_pd_html: "I consider my contributions to be in the %{consider_pd_link}."
-      consider_pd: "public domain"
-      consider_pd_url: https://osmfoundation.org/wiki/Licence_and_Legal_FAQ/Why_would_I_want_my_contributions_to_be_public_domain
       or: "or"
       use external auth: "or sign up with a third party"
     no_such_user:
@@ -2802,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
@@ -2905,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:
@@ -3318,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 45fc19f2c295f3ff3834fd92ed9430d83de23416..b3fc5af15900002fe06d4fe8e5cef36315528d5e 100644 (file)
@@ -121,7 +121,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
@@ -298,7 +298,9 @@ OpenStreetMap::Application.routes.draw do
   resource :account, :only => [:edit, :update, :destroy] do
     scope :module => :accounts do
       resource :terms, :only => [:show, :update]
+      resource :pd_declaration, :only => [:show, :create]
       resource :deletion, :only => :show
+      resource :home, :only => :show
     end
   end
 
index b0e2f461392cce37eb7221ad9f72e0601f7a361c..b7cffcc27b33df6ef361155d832cbc265a1f30a2 100644 (file)
@@ -53,3 +53,5 @@ doorkeeper_signing_key: |
   cK1+/2V+OkM/0nXjxPwPj7LiOediUyZNUn48r29uGOL1S83PSUdyST207CP6mZjc
   K8aJmnGsVEAcWPzbpNh14q/c
   -----END PRIVATE KEY-----
+# 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..852f270b6ef713571339a1ada651a4c3ed219826 100644 (file)
@@ -9,6 +9,7 @@ end
 class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
   driven_by :selenium, :using => :headless_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
diff --git a/test/controllers/accounts/pd_declarations_controller_test.rb b/test/controllers/accounts/pd_declarations_controller_test.rb
new file mode 100644 (file)
index 0000000..be0d46f
--- /dev/null
@@ -0,0 +1,92 @@
+require "test_helper"
+
+module Accounts
+  class PdDeclarationsControllerTest < ActionDispatch::IntegrationTest
+    ##
+    # test all routes which lead to this controller
+    def test_routes
+      assert_routing(
+        { :path => "/account/pd_declaration", :method => :get },
+        { :controller => "accounts/pd_declarations", :action => "show" }
+      )
+      assert_routing(
+        { :path => "/account/pd_declaration", :method => :post },
+        { :controller => "accounts/pd_declarations", :action => "create" }
+      )
+    end
+
+    def test_show_not_logged_in
+      get account_pd_declaration_path
+
+      assert_redirected_to login_path(:referer => account_pd_declaration_path)
+    end
+
+    def test_show_agreed
+      user = create(:user)
+      session_for(user)
+
+      get account_pd_declaration_path
+
+      assert_response :success
+    end
+
+    def test_create_not_logged_in
+      post account_pd_declaration_path
+
+      assert_response :forbidden
+    end
+
+    def test_create_unconfirmed
+      user = create(:user)
+      session_for(user)
+
+      post account_pd_declaration_path
+
+      assert_redirected_to edit_account_path
+      assert_nil flash[:notice]
+      assert_equal "You didn't confirm that you consider your edits to be in the Public Domain.", flash[:warning]
+
+      user.reload
+      assert_not_predicate user, :consider_pd
+    end
+
+    def test_create_confirmed
+      user = create(:user)
+      session_for(user)
+
+      post account_pd_declaration_path, :params => { :consider_pd => true }
+
+      assert_equal "You have successfully declared that you consider your edits to be in the Public Domain.", flash[:notice]
+      assert_nil flash[:warning]
+
+      user.reload
+      assert_predicate user, :consider_pd
+    end
+
+    def test_create_already_declared_unconfirmed
+      user = create(:user, :consider_pd => true)
+      session_for(user)
+
+      post account_pd_declaration_path
+
+      assert_nil flash[:notice]
+      assert_equal "You have already declared that you consider your edits to be in the Public Domain.", flash[:warning]
+
+      user.reload
+      assert_predicate user, :consider_pd
+    end
+
+    def test_create_already_declared_confirmed
+      user = create(:user, :consider_pd => true)
+      session_for(user)
+
+      post account_pd_declaration_path, :params => { :consider_pd => true }
+
+      assert_nil flash[:notice]
+      assert_equal "You have already declared that you consider your edits to be in the Public Domain.", flash[:warning]
+
+      user.reload
+      assert_predicate user, :consider_pd
+    end
+  end
+end
index 76888466626f049f98860f8d11fc3c1a0bf48fb8..55b30506b41f1bba226869a57125ba088e91bb9b 100644 (file)
@@ -52,13 +52,12 @@ module Accounts
       user = create(:user, :terms_seen => false, :terms_agreed => nil)
       session_for(user)
 
-      put account_terms_path, :params => { :user => { :consider_pd => true }, :read_ct => 1, :read_tou => 1 }
+      put account_terms_path, :params => { :read_ct => 1, :read_tou => 1 }
       assert_redirected_to edit_account_path
       assert_equal "Thanks for accepting the new contributor terms!", flash[:notice]
 
       user.reload
 
-      assert user.consider_pd
       assert_not_nil user.terms_agreed
       assert user.terms_seen
     end
@@ -67,13 +66,12 @@ module Accounts
       user = create(:user, :terms_seen => false, :terms_agreed => nil)
       session_for(user)
 
-      put account_terms_path, :params => { :user => { :consider_pd => true }, :referer => "/test", :read_ct => 1, :read_tou => 1 }
+      put account_terms_path, :params => { :referer => "/test", :read_ct => 1, :read_tou => 1 }
       assert_redirected_to "/test"
       assert_equal "Thanks for accepting the new contributor terms!", flash[:notice]
 
       user.reload
 
-      assert user.consider_pd
       assert_not_nil user.terms_agreed
       assert user.terms_seen
     end
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
index 1a53f62daadea43a37615c63777b23edfce4ab7e..5d75c508dd80f3bed6deecaf2f6b8cddd016ac90 100644 (file)
@@ -34,8 +34,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => dup_email,
                                        :display_name => display_name,
                                        :pass_crypt => "testtest",
-                                       :pass_crypt_confirmation => "testtest",
-                                       :consider_pd => "1" } }
+                                       :pass_crypt_confirmation => "testtest" } }
         end
       end
     end
@@ -57,8 +56,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :pass_crypt => "testtest",
                                        :pass_crypt_confirmation => "testtest",
                                        :auth_provider => "google",
-                                       :auth_uid => "123454321",
-                                       :consider_pd => "1" } }
+                                       :auth_uid => "123454321" } }
         end
       end
     end
@@ -97,8 +95,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => email,
                                        :display_name => display_name,
                                        :pass_crypt => "testtest",
-                                       :pass_crypt_confirmation => "blahblah",
-                                       :consider_pd => "1" } }
+                                       :pass_crypt_confirmation => "blahblah" } }
         end
       end
     end
@@ -117,8 +114,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => email,
                                        :display_name => dup_display_name,
                                        :auth_provider => "google",
-                                       :auth_uid => "123454321",
-                                       :consider_pd => "1" } }
+                                       :auth_uid => "123454321" } }
         end
       end
     end
@@ -138,8 +134,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :pass_crypt => "testtest",
-                                       :pass_crypt_confirmation => "testtest",
-                                       :consider_pd => "1" } }
+                                       :pass_crypt_confirmation => "testtest" } }
           assert_redirected_to :controller => :confirmations, :action => :confirm, :display_name => display_name
           follow_redirect!
         end
@@ -192,8 +187,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :pass_crypt => password,
-                                       :pass_crypt_confirmation => password,
-                                       :consider_pd => "1" },
+                                       :pass_crypt_confirmation => password },
                             :referer => referer }
           assert_response(:redirect)
           assert_redirected_to :controller => :confirmations, :action => :confirm, :display_name => display_name
@@ -254,8 +248,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :auth_provider => "openid",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
         end
       end
     end
@@ -330,8 +323,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :auth_provider => "openid",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           follow_redirect!
         end
       end
@@ -392,8 +384,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :auth_provider => "google",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" },
+                                       :auth_uid => auth_uid },
                             :email_hmac => email_hmac }
           assert_redirected_to welcome_path
           follow_redirect!
@@ -479,8 +470,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :email_hmac => email_hmac,
                                        :display_name => display_name,
                                        :auth_provider => "google",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           assert_response :redirect
           follow_redirect!
         end
@@ -541,8 +531,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :auth_provider => "facebook",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" },
+                                       :auth_uid => auth_uid },
                             :email_hmac => email_hmac }
           assert_redirected_to welcome_path
           follow_redirect!
@@ -628,8 +617,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :email_hmac => email_hmac,
                                        :display_name => display_name,
                                        :auth_provider => "facebook",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           assert_response :redirect
           follow_redirect!
         end
@@ -689,8 +677,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                :params => { :user => { :email => new_email,
                                        :display_name => display_name,
                                        :auth_provider => "microsoft",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" },
+                                       :auth_uid => auth_uid },
                             :email_hmac => email_hmac }
           assert_redirected_to welcome_path
           follow_redirect!
@@ -775,8 +762,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :email_hmac => email_hmac,
                                        :display_name => display_name,
                                        :auth_provider => "microsoft",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           assert_response :redirect
           follow_redirect!
         end
@@ -926,8 +912,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :email_hmac => email_hmac,
                                        :display_name => display_name,
                                        :auth_provider => "github",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           assert_response :redirect
           follow_redirect!
         end
@@ -1076,8 +1061,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest
                                        :email_hmac => email_hmac,
                                        :display_name => display_name,
                                        :auth_provider => "wikipedia",
-                                       :auth_uid => auth_uid,
-                                       :consider_pd => "1" } }
+                                       :auth_uid => auth_uid } }
           assert_response :redirect
           follow_redirect!
         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
diff --git a/test/system/account_pd_declaration_test.rb b/test/system/account_pd_declaration_test.rb
new file mode 100644 (file)
index 0000000..d58484c
--- /dev/null
@@ -0,0 +1,46 @@
+require "application_system_test_case"
+
+class AccountPdDeclarationTest < ApplicationSystemTestCase
+  def setup
+    @user = create(:user, :display_name => "test user")
+    sign_in_as(@user)
+  end
+
+  test "can decline declaration if no declaration was made" do
+    visit account_pd_declaration_path
+
+    within_content_body do
+      assert_unchecked_field "I consider my contributions to be in the Public Domain"
+      assert_button "Confirm"
+
+      click_on "Confirm"
+
+      assert_no_text "You have also declared that you consider your edits to be in the Public Domain."
+    end
+  end
+
+  test "can confirm declaration if no declaration was made" do
+    visit account_pd_declaration_path
+
+    within_content_body do
+      assert_unchecked_field "I consider my contributions to be in the Public Domain"
+      assert_button "Confirm"
+
+      check "I consider my contributions to be in the Public Domain"
+      click_on "Confirm"
+
+      assert_text "You have also declared that you consider your edits to be in the Public Domain."
+    end
+  end
+
+  test "show disabled checkbox if declaration was made" do
+    @user.update(:consider_pd => true)
+
+    visit account_pd_declaration_path
+
+    within_content_body do
+      assert_checked_field "I consider my contributions to be in the Public Domain", :disabled => true
+      assert_button "Confirm", :disabled => true
+    end
+  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..c14045b1c0179b929f556c4c87fa615db71e16a8 100644 (file)
@@ -100,10 +100,12 @@ 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(:args => ["-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
     }
   }