]> git.openstreetmap.org Git - rails.git/commitdiff
Merge pull request #4353 from tomhughes/rtlcss
authorAndy Allan <git@gravitystorm.co.uk>
Wed, 22 Nov 2023 17:13:35 +0000 (17:13 +0000)
committerGitHub <noreply@github.com>
Wed, 22 Nov 2023 17:13:35 +0000 (17:13 +0000)
Improve handling of RTL styling

30 files changed:
.rubocop_todo.yml
Gemfile.lock
app/controllers/accounts_controller.rb
app/controllers/api/changesets_controller.rb
app/controllers/api/notes_controller.rb
app/controllers/changesets_controller.rb
app/models/changeset.rb
app/models/user.rb
app/views/account/deletions/show.html.erb
app/views/api/changesets/changeset.xml.builder
app/views/api/changesets/changesets.xml.builder
config/initializers/config.rb
config/locales/br.yml
config/locales/en.yml
config/locales/eo.yml
config/locales/fr.yml
config/locales/ia.yml
config/locales/ko.yml
config/locales/pt-PT.yml
config/locales/pt.yml
config/locales/ru.yml
config/locales/sv.yml
config/locales/zh-TW.yml
config/settings.yml
db/migrate/20231117170422_add_closed_at_index_to_changesets.rb [new file with mode: 0644]
db/structure.sql
test/controllers/accounts_controller_test.rb
test/models/user_test.rb
test/system/account_deletion_test.rb
test/test_helper.rb

index e3407c6dc0111d811f1b34377429a46d63a2d14a..bd2f92309886c83b83e4dfdaecd5cc8153f23cc7 100644 (file)
@@ -61,7 +61,7 @@ Metrics/BlockNesting:
 # Offense count: 26
 # Configuration parameters: CountComments, CountAsOne.
 Metrics/ClassLength:
-  Max: 286
+  Max: 297
 
 # Offense count: 59
 # Configuration parameters: AllowedMethods, AllowedPatterns.
index fc67f9790cc923c681d30510289cdca7ce3e84e2..885de22b4aecce9c82328445fac01bd87c70a7a1 100644 (file)
@@ -65,7 +65,7 @@ GEM
       activemodel (= 7.1.2)
       activesupport (= 7.1.2)
       timeout (>= 0.4.0)
-    activerecord-import (1.5.0)
+    activerecord-import (1.5.1)
       activerecord (>= 4.2)
     activestorage (7.1.2)
       actionpack (= 7.1.2)
@@ -95,8 +95,8 @@ GEM
     autoprefixer-rails (10.4.15.0)
       execjs (~> 2)
     aws-eventstream (1.2.0)
-    aws-partitions (1.849.0)
-    aws-sdk-core (3.186.0)
+    aws-partitions (1.854.0)
+    aws-sdk-core (3.187.1)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.5)
@@ -104,7 +104,7 @@ GEM
     aws-sdk-kms (1.72.0)
       aws-sdk-core (~> 3, >= 3.184.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.136.0)
+    aws-sdk-s3 (1.137.0)
       aws-sdk-core (~> 3, >= 3.181.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.6)
@@ -233,7 +233,7 @@ GEM
     factory_bot_rails (6.2.0)
       factory_bot (~> 6.2.0)
       railties (>= 5.0.0)
-    faraday (2.7.11)
+    faraday (2.7.12)
       base64
       faraday-net_http (>= 2.0, < 3.1)
       ruby2_keywords (>= 0.0.4)
@@ -251,7 +251,7 @@ GEM
       ffi (>= 1.0.0)
     globalid (1.2.1)
       activesupport (>= 6.1)
-    google-protobuf (3.25.0)
+    google-protobuf (3.25.1)
     hashdiff (1.0.1)
     hashie (5.0.0)
     highline (2.1.0)
@@ -288,7 +288,7 @@ GEM
     image_size (3.3.0)
     in_threads (1.6.0)
     io-console (0.6.0)
-    irb (1.9.0)
+    irb (1.9.1)
       rdoc
       reline (>= 0.3.8)
     jbuilder (2.11.5)
@@ -343,8 +343,8 @@ GEM
       timeout
     net-smtp (0.4.0)
       net-protocol
-    nio4r (2.5.9)
-    nokogiri (1.15.4)
+    nio4r (2.6.1)
+    nokogiri (1.15.5)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
     oauth (0.4.7)
@@ -402,7 +402,7 @@ GEM
     progress (3.6.0)
     psych (5.1.1.1)
       stringio
-    public_suffix (5.0.3)
+    public_suffix (5.0.4)
     puma (5.6.7)
       nio4r (~> 2.0)
     quad_tile (1.0.1)
@@ -499,10 +499,11 @@ GEM
     rubocop-performance (1.19.1)
       rubocop (>= 1.7.0, < 2.0)
       rubocop-ast (>= 0.4.0)
-    rubocop-rails (2.22.1)
+    rubocop-rails (2.22.2)
       activesupport (>= 4.2.0)
       rack (>= 1.1)
       rubocop (>= 1.33.0, < 2.0)
+      rubocop-ast (>= 1.30.0, < 2.0)
     rubocop-rake (0.6.0)
       rubocop (~> 1.0)
     ruby-openid (2.9.2)
index 63da1293ff731ecfe85d0430f9c6f0f1819a2008..db972101088b2a958fa0f3ffe25798fec41f1bbf 100644 (file)
@@ -53,12 +53,16 @@ class AccountsController < ApplicationController
   end
 
   def destroy
-    current_user.soft_destroy!
+    if current_user.deletion_allowed?
+      current_user.soft_destroy!
 
-    session.delete(:user)
-    session_expires_automatically
+      session.delete(:user)
+      session_expires_automatically
 
-    flash[:notice] = t ".success"
-    redirect_to root_path
+      flash[:notice] = t ".success"
+      redirect_to root_path
+    else
+      head :bad_request
+    end
   end
 end
index 9bdf0f2bd3b075a5a5eb09e5755bfa76ee224bde..c9c806de6ecc4a7e19d2d345180c87ea3d643493 100644 (file)
@@ -283,7 +283,6 @@ module Api
     ##
     # if a bounding box was specified do some sanity checks.
     # restrict changesets to those enclosed by a bounding box
-    # we need to return both the changesets and the bounding box
     def conditions_bbox(changesets, bbox)
       if bbox
         bbox.check_boundaries
index 95466781f84c8bd97a8fef6c6bea1332d26f6388..e28c0a622802f7d1419e200928e0a5893374d550 100644 (file)
@@ -389,8 +389,14 @@ module Api
     def add_comment(note, text, event, notify: true)
       attributes = { :visible => true, :event => event, :body => text }
 
-      if current_user
-        attributes[:author_id] = current_user.id
+      if doorkeeper_token || current_token
+        author = current_user if scope_enabled?(:write_notes)
+      else
+        author = current_user
+      end
+
+      if author
+        attributes[:author_id] = author.id
       else
         attributes[:author_ip] = request.remote_ip
       end
index fef4d85eb51273596dfe81ccfc0cad88ba81c19f..859242b60fff5106ec2361b724f2972c5d423ab1 100644 (file)
@@ -83,7 +83,6 @@ class ChangesetsController < ApplicationController
   ##
   # if a bounding box was specified do some sanity checks.
   # restrict changesets to those enclosed by a bounding box
-  # we need to return both the changesets and the bounding box
   def conditions_bbox(changesets, bbox)
     if bbox
       bbox.check_boundaries
index ce09438245844be0ff4ba241818973cdf26b5bab..137de18fd6479640c42688df2aceff3f56f1bfbb 100644 (file)
 #
 # Indexes
 #
-#  changesets_bbox_idx                (min_lat,max_lat,min_lon,max_lon) USING gist
-#  changesets_closed_at_idx           (closed_at)
-#  changesets_created_at_idx          (created_at)
-#  changesets_user_id_created_at_idx  (user_id,created_at)
-#  changesets_user_id_id_idx          (user_id,id)
+#  changesets_bbox_idx                        (min_lat,max_lat,min_lon,max_lon) USING gist
+#  changesets_closed_at_idx                   (closed_at)
+#  changesets_created_at_idx                  (created_at)
+#  changesets_user_id_created_at_idx          (user_id,created_at)
+#  changesets_user_id_id_idx                  (user_id,id)
+#  index_changesets_on_user_id_and_closed_at  (user_id,closed_at)
 #
 # Foreign Keys
 #
index 7571dd9dc5c8011f4615ebf970b6f0daa377adc9..1942a25cc237a784394b91f9ba64c844f36e3309 100644 (file)
@@ -419,6 +419,18 @@ class User < ApplicationRecord
     end
   end
 
+  def deletion_allowed_at
+    unless Settings.user_account_deletion_delay.nil?
+      last_changeset = changesets.reorder(:closed_at => :desc).first
+      return last_changeset.closed_at.utc + Settings.user_account_deletion_delay.hours if last_changeset
+    end
+    creation_time.utc
+  end
+
+  def deletion_allowed?
+    deletion_allowed_at <= Time.now.utc
+  end
+
   private
 
   def encrypt_password
index ddc8216772878300b3348b5ff55fd4f6608dbcc0..0ed4d663f03315944f9f7d5b1df1e82a81042f7e 100644 (file)
   <li><%= t ".retain_email" %></li>
 </ul>
 
-<%= link_to t(".delete_account"), account_path, { :method => :delete, :class => "btn btn-danger", :data => { :confirm => t(".confirm_delete") } } %>
+<% if current_user.deletion_allowed? %>
+  <%= link_to t(".delete_account"), account_path, { :method => :delete, :class => "btn btn-danger", :data => { :confirm => t(".confirm_delete") } } %>
+<% else %>
+  <div class="alert alert-warning">
+    <%= t ".recent_editing_html", :time => friendly_date(current_user.deletion_allowed_at) %>
+  </div>
+  <button class="btn btn-secondary" disabled><%= t(".delete_account") %></button>
+<% end %>
+
 <%= link_to t(".cancel"), edit_account_path, :class => "btn btn-link" %>
index cedfc81ea5ce34310cac041136bf9d5a39030f4f..b53061d4c347695c0d449d7d509e90792f683bbe 100644 (file)
@@ -1,7 +1,5 @@
 xml.instruct! :xml, :version => "1.0"
 
-# basic attributes
-
 xml.osm(OSM::API.new.xml_root_attributes) do |osm|
   osm << render(@changeset)
 end
index c68a76da3bcf5da5746f2f28e7a61d9dc4b42cfb..5ff7e262044032609666cd2deeada933bb917fb6 100644 (file)
@@ -1,7 +1,5 @@
 xml.instruct! :xml, :version => "1.0"
 
-# basic attributes
-
 xml.osm(OSM::API.new.xml_root_attributes) do |osm|
   @changesets.each do |changeset|
     osm << render(changeset)
index c1cc522a5e13b24eef5b2bae373c512d403b82f6..f36e6ac1ac4f0686dc401f928c552fe1695478b4 100644 (file)
@@ -79,6 +79,7 @@ Config.setup do |config|
     required(:max_number_of_relation_members).filled(:int?)
     required(:max_issues_count).filled(:int?)
     required(:api_timeout).filled(:int?)
+    required(:user_account_deletion_delay).maybe(:number?)
     required(:imagery_blacklist).maybe(:array?)
     required(:status).filled(:str?, :included_in? => ALLOWED_STATUS)
     required(:avatar_storage).filled(:str?)
index c3c4a5b49be7ef351e36fb8c72056139111733ba..f7ce20268e166cdd4e41e4681301ad34fd2f1f6d 100644 (file)
@@ -776,6 +776,7 @@ br:
           ranger_station: Post gward-koad
           recycling: Lec'h adaozañ
           restaurant: Preti
+          sanitary_dump_station: Savlec'h pompañ an dourioù lous
           school: Skol
           shelter: Gwasked
           shower: Strinkadenn
@@ -852,6 +853,7 @@ br:
           roof: Toenn
           ruins: Savadur dismantret
           school: Savadur skol
+          semidetached_house: Ti stag
           service: Savadur servij
           shed: Lochenn
           stable: Marchosi
@@ -863,6 +865,7 @@ br:
           warehouse: Sanailh
           "yes": Savadur
         club:
+          scout: Diazlec'h ur strollad skout
           sport: Kleub sport
           "yes": Klub
         craft:
@@ -874,6 +877,7 @@ br:
           confectionery: Koñfizerezh
           dressmaker: Kemener
           electrician: Tredanour
+          electronics_repair: Dreser dafar tredanek
           gardener: Liorzhour
           glaziery: Gweraerezh
           handicraft: Artizanerezh
@@ -1975,6 +1979,9 @@ br:
       legal_1_1_terms_of_use: Termenoù implij
       legal_1_1_privacy_policy: Politikerezh prevezded
       legal_2_1_contact_the_osmf: mont e darempred gant diazezadur OSM
+      legal_2_2_html: OpenStreetMap, al logo gant al loupenn hag ar State of the Map
+        a zo %{registered_trademarks_link}.
+      legal_2_2_registered_trademarks: merkoù marilhet an OSMF
       partners_title: Kevelerien
     copyright:
       foreign:
index 1a41dcce814de5f00882f7393c780f5b616f12c6..56d722f4331da17cd17d9145258f48c8f698a9f5 100644 (file)
@@ -256,6 +256,7 @@ en:
         retain_notes: Your map notes and note comments, if any, will be retained but hidden from view.
         retain_changeset_discussions: Your changeset discussions, if any, will be retained.
         retain_email: Your email address will be retained.
+        recent_editing_html: "As you have edited recently your account cannot currently be deleted. Deletion will be possible in %{time}."
         confirm_delete: Are you sure?
         cancel: Cancel
   accounts:
index c2434fdde5034c6a2b3095035f40181bb4f058bd..259a64a7fef761b81c1fcf72b640626d3a6566e7 100644 (file)
@@ -2778,12 +2778,15 @@ eo:
       role:
         administrator: Ĉi tiu uzanto estas administranto
         moderator: Ĉi tiu uzanto estas kontrolanto
+        importer: Tiu ĉi uzanto estas enportisto
         grant:
           administrator: Permesi aliron de administranto
           moderator: Permesi aliron de kontrolanto
+          importer: Permesi aliron de enportisto
         revoke:
           administrator: Nuligi aliron de administranto
           moderator: Nuligi aliron de kontrolanto
+          importer: Nuligi aliron de enportisto
       block_history: Blokadoj aktivaj
       moderator_history: Blokadoj eldonitaj
       comments: Komentoj
index c3ff91afde87315eb1b8b67274701e73f71685be..d3f258a5b911c94335634733767ca15183b578d3 100644 (file)
@@ -2109,7 +2109,7 @@ fr:
         si vous avez des questions sur les conditions de licence, les droits d’auteur ou d’autres questions juridiques.
       legal_2_1_contact_the_osmf: contacter l’OSMF
       legal_2_2_html: OpenStreetMap, le logo avec la loupe, ainsi que State of the
-        Map are %{registered_trademarks_link}.
+        Map sont des %{registered_trademarks_link}.
       legal_2_2_registered_trademarks: marques commerciales enregistrées de l’OSMF
       partners_title: Partenaires
     copyright:
index 7aa59b5a15d51f1cf445c772cf0099c1ff3ad424..12e5025452cbced0d835b10faf29d36c254dbbb3 100644 (file)
@@ -2810,12 +2810,15 @@ ia:
       role:
         administrator: Iste usator es un administrator
         moderator: Iste usator es un moderator
+        importer: Iste usator es un importator
         grant:
           administrator: Conceder accesso de administrator
           moderator: Conceder accesso de moderator
+          importer: Conceder le accesso de importator
         revoke:
           administrator: Revocar accesso de administrator
           moderator: Revocar accesso de moderator
+          importer: Revocar le accesso de importator
       block_history: Blocadas active
       moderator_history: Blocadas imponite
       comments: Commentos
index 6ae22c6364820dfdb527b9bb833fe2c4495b0e49..62ccce407c1555db3954adc4220207735cf05ff1 100644 (file)
@@ -834,23 +834,30 @@ ko:
           gardener: 정원사
           glaziery: 유리 공장
           handicraft: 수공업체
+          metal_construction: 금속 시공업자
           painter: 화가
           photographer: 사진 작가
           plumber: 배관공
+          roofer: 지붕 시공업자
           sawmill: 제재소
           shoemaker: 구두공
           stonemason: 석공
           tailor: 재단사
+          window_construction: 창문 시공업자
           winery: 포도주 양조장
           "yes": 공예품점
         emergency:
+          access_point: 비상시 구조 지점
           ambulance_station: 구급 의료 센터
           assembly_point: 집합 장소
           defibrillator: 제세동기
+          fire_extinguisher: 소화기
           fire_water_pond: 방화용 연못
           landing_site: 비상 착륙지
+          life_ring: 구명부표
           phone: 긴급 전화
           siren: 비상 사이렌
+          suction_point: 소방용 수원지
           water_tank: 긴급 물탱크
         highway:
           abandoned: 버려진 고속도로
@@ -863,6 +870,7 @@ ko:
           cycleway: 자전거 전용도로
           elevator: 엘리베이터
           emergency_access_point: 긴급 액세스 포인트
+          emergency_bay: 비상주차대
           footway: 보도
           ford: 여울
           give_way: 양보 표지
@@ -895,6 +903,7 @@ ko:
           track: 오솔길
           traffic_mirror: 도로반사경
           traffic_signals: 교통 신호
+          trailhead: 산책로 기점
           trunk: 간선 도로
           trunk_link: 간선 도로
           turning_loop: 방향전환 운전용 루프선
@@ -1060,6 +1069,7 @@ ko:
           "yes": 산길
         natural:
           atoll: 환초
+          bare_rock: 노출 암반
           bay: 만
           beach: 해변
           cape: 곶
@@ -1078,6 +1088,7 @@ ko:
           hill: 언덕
           hot_spring: 온천
           island: 섬
+          isthmus: 지협
           land: 토지
           marsh: 습지
           moor: 습지
@@ -1145,6 +1156,7 @@ ko:
           locality: 지역
           municipality: 지방자치체
           neighbourhood: 마을
+          plot: 대지
           postcode: 우편 번호
           quarter: 구역
           region: 지역
@@ -1158,6 +1170,7 @@ ko:
           "yes": 장소
         railway:
           abandoned: 폐선된 철도
+          buffer_stop: 철도 차막이
           construction: 건설 중인 철도
           disused: 폐선된 철도
           funicular: 케이블 카
@@ -1193,6 +1206,7 @@ ko:
           bakery: 제과점
           bathroom_furnishing: 욕실 설치 업체
           beauty: 미용실
+          bed: 침구점
           beverages: 음료 가게
           bicycle: 자전거 가게
           bookmaker: 마권업자
@@ -1306,6 +1320,7 @@ ko:
           attraction: 관광 명소
           bed_and_breakfast: 민박
           cabin: 여행자 오두막
+          camp_pitch: 캠프장 피치
           camp_site: 캠프장
           caravan_site: 캐러밴 사이트
           chalet: 샬렛
@@ -1846,14 +1861,12 @@ ko:
       legal_title: 법률
       legal_1_1_html: |-
         본 사이트와 기타 관련 서비스의 운영은 커뮤니티의 위탁으로
-        %{openstreetmap_foundation_link}(OSMF)가 공식적으로 맡고 있습니다. OSMF가 운영하는 모든 서비스의 이용 시 %{terms_of_use_link}, %{aup_link}, %{privacy_policy_link} 적용됩니다.
+        %{openstreetmap_foundation_link}(OSMF)가 공식적으로 맡고 있습니다. OSMF가 운영하는 모든 서비스의 이용 시 %{terms_of_use_link}, %{aup_link}, %{privacy_policy_link} 적용됩니다.
       legal_1_1_openstreetmap_foundation: 오픈스트리트맵 재단
       legal_1_1_terms_of_use: 이용 약관
       legal_1_1_aup: 사용 범위 정책
       legal_1_1_privacy_policy: 개인정보처리방침
-      legal_2_1_html: |-
-        라이선스, 저작권 또는 그 밖의 법무 관련 질문이 있는 경우 %{contact_the_osmf_link}
-        으로 문의하세요.
+      legal_2_1_html: 라이선스, 저작권 또는 그 밖의 법무 관련 질문이 있는 경우 %{contact_the_osmf_link}.
       legal_2_1_contact_the_osmf: OSMF에 문의하세요
       legal_2_2_html: 오픈스트리트맵 (OpenStreetMap)과 지도 위 돋보기 로고는 %{registered_trademarks_link}입니다.
       legal_2_2_registered_trademarks: OSMF의 등록 상표
@@ -2512,12 +2525,13 @@ ko:
       email_confirmation_help_html: 당신의 주소는 공개적으로 노출되지 않습니다. 자세한 내용은 %{privacy_policy_link}를
         참조하세요.
       privacy_policy: 개인정보처리방침
+      privacy_policy_title: 이메일 주소 섹션을 포함한 OSMF 개인 정보 보호 정책
     terms:
       title: 약관
       heading: 약관
       heading_ct: 기여자 약관
       read and accept with tou: 기여자 동의 및 사용 약관을 읽고 두 개의 체크 상자에 체크한 다음 계속 버튼을 눌러 주십시오.
-      contributor_terms_explain: 이 동의는 기존 기여와 앞으로의 기여자의 약관에 적용됩니다.
+      contributor_terms_explain: 본 동의는 기존 기여분과 추후 기여분의 이용 약관에 적용됩니다.
       read_ct: 상기의 기여자 약관을 읽었고 동의합니다
       tou_explain_html: 이러한 %{tou_link}는 오픈스트리트맵 재단에서 제공하는 웹사이트 및 기타 인프라 사용을 관리합니다.
         링크를 클릭하여 텍스트를 읽고 동의하십시오.
@@ -2525,6 +2539,10 @@ ko:
       consider_pd: 위의 내용 외에도 내 기여가 퍼블릭 도메인에 있다고 간주합니다
       consider_pd_why: 무엇인가요?
       consider_pd_why_url: https://wiki.osmfoundation.org/wiki/Licence_and_Legal_FAQ/Why_would_I_want_my_contributions_to_be_public_domain
+      guidance_info_html: '이러한 용어를 이해하는 데 도움이 되는 정보: %{readable_summary_link} / 일부
+        %{informal_translations_link}'
+      readable_summary: 해석 요약본
+      informal_translations: 비공식 번역
       continue: 계속
       declined: https://wiki.openstreetmap.org/wiki/Contributor_Terms_Declined
       decline: 거부
@@ -2577,18 +2595,22 @@ ko:
       role:
         administrator: 이 사용자는 관리자입니다
         moderator: 이 사용자는 운영자입니다
+        importer: 이 사용자는 임포터입니다
         grant:
           administrator: 관리자 권한 부여
           moderator: 운영자 권한 부여
+          importer: 임포터 접근 권한 부여
         revoke:
           administrator: 관리자 권한 해제
           moderator: 운영자 권한 해제
+          importer: 임포터 접근 권한 취소
       block_history: 활성화된 차단
       moderator_history: 실행된 차단
       comments: 의견
       create_block: 이 사용자를 차단
       activate_user: 이 사용자 활성화
       confirm_user: 이 사용자 확인
+      unconfirm_user: 이 사용자 확인 취소
       unsuspend_user: 이 사용자 정지 해제
       hide_user: 이 사용자를 숨기기
       unhide_user: 이 사용자를 숨기기 취소
index fdb3db46ccd867483ee2ddcf12568717e3b13ad0..d9534088ac3fc39c20a934d0a24ec1c3f4ff7cb9 100644 (file)
@@ -29,6 +29,7 @@
 # Author: Malafaya
 # Author: Mansil
 # Author: Mansil alfalb
+# Author: Matheusgomesms
 # Author: McDutchie
 # Author: MokaAkashiyaPT
 # Author: Nemo bis
@@ -268,6 +269,7 @@ pt-PT:
         reopened_at_by_html: Reaberto %{when} por %{user}
       rss:
         title: Notas OpenStreetMap
+        description_all: Uma lista de notas denunciadas, comentadas ou fechadas.
         description_area: Lista de notas reportadas, comentadas ou resolvidas na tua
           área [(%{min_lat}|%{min_lon}) -- (%{max_lat}|%{max_lon})]
         description_item: Um feed RSS para a nota %{id} reportada
@@ -624,10 +626,39 @@ pt-PT:
       newer_comments: Comentários mais recentes
       older_comments: Comentários mais antigos
   doorkeeper:
+    errors:
+      messages:
+        account_selection_required: O servidor de autorização requer seleção de conta
+          de utilizador final
+        consent_required: O servidor de autorização requer o consentimento do utilizador
+          final
+        interaction_required: O servidor de autorização requer interação com o utilizador
+          final
+        login_required: O servidor de autorização requer autenticação do utilizador
+          final
     flash:
       applications:
         create:
           notice: Aplicação registada.
+    openid_connect:
+      errors:
+        messages:
+          auth_time_from_resource_owner_not_configured: Falha devido a ausência de
+            configuração de Doorkeeper::OpenidConnect.configure.auth_time_from_resource_owner.
+          reauthenticate_resource_owner_not_configured: Falha devido a ausência de
+            configuração de Doorkeeper::OpenidConnect.configure.reauthenticate_resource_owner.
+          resource_owner_from_access_token_not_configured: Falha devido a ausência
+            de configuração de Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token.
+          select_account_for_resource_owner_not_configured: Falha devido a ausência
+            de configuração de Doorkeeper::OpenidConnect.configure.select_account_for_resource_owner.
+          subject_not_configured: Falha na geração de token ID devido a ausência de
+            configuração de Doorkeeper::OpenidConnect.configure.subject.
+    scopes:
+      address: Ver o teu endereço físico
+      email: Ver o teu endereço eletrónico
+      openid: Autenticar a tua conta
+      phone: Ver o teu número de telefone
+      profile: Ver a tua informação de perfil
   errors:
     contact:
       contact_url_title: Vários canais de contacto explicados
@@ -2078,6 +2109,11 @@ pt-PT:
           de Recursos Naturais do Canadá) e StatCan (Divisão de Geografia, Estatísticas
           do Canadá).'
         contributors_ca_canada: Canadá
+        contributors_cz_credit_html: '%{czechia}: contém dados da Administração Estatal
+          de Topografia e Cadastro sob licença %{cc_licence_link}'
+        contributors_cz_czechia: Chéquia
+        contributors_cz_cc_licence: Atribuição Creative Commons 4.0 Licença Internacional
+          (CC BY 4.0)
         contributors_fi_credit_html: '%{finland}: Contém dados do Serviço Topográfico
           Nacional da Base de Dados Topográfica da Finlândia e outros conjuntos de
           dados, sob a %{nlsfi_license_link}.'
@@ -2597,6 +2633,7 @@ pt-PT:
     permissions:
       missing: Não deste permissão à aplicação para aceder a este serviço
     scopes:
+      openid: Iniciar sessão com OpenStreetMap
       read_prefs: Ler preferências de utilizador
       write_prefs: Modificar preferências de utilizador
       write_diary: Criar entradas de diário, comentar e fazer amigos
@@ -2791,6 +2828,7 @@ pt-PT:
       remove as friend: Remover amigo
       add as friend: Adicionar aos amigos
       mapper since: 'A mapear desde:'
+      uid: 'Id de utilizador:'
       ct status: 'Termos do Contribuidor:'
       ct undecided: Por decidir
       ct declined: Rejeitado
@@ -2801,12 +2839,15 @@ pt-PT:
       role:
         administrator: Este utilizador é um administrador
         moderator: Este utilizador é um moderador
+        importer: Este utilizador é importador
         grant:
           administrator: Dar acesso de administrador
           moderator: Dar acesso de moderador
+          importer: Conceder acesso de importador
         revoke:
           administrator: Retirar acesso de administrador
           moderator: Retirar acesso de moderador
+          importer: Revogar acesso de importador
       block_history: Bloqueios ativos
       moderator_history: Bloqueios aplicados
       comments: Comentários
@@ -3075,6 +3116,7 @@ pt-PT:
         cyclosm: CyclOSM
         cycle_map: Mapa de Ciclismo
         transport_map: Transportes Públicos
+        tracestracktop_topo: Tracestrack Topo
         hot: Humanitário
         opnvkarte: ÖPNVKarte
       layers:
@@ -3093,6 +3135,7 @@ pt-PT:
       andy_allan: Andy Allan
       opnvkarte_credit: Mosaicos cortesia de %{memomaps_link}
       memomaps: MeMoMaps
+      tracestrack_credit: Mosaicos cortesia de %{tracestrack_link}
       hotosm_credit: Estilo de mosaicos por %{hotosm_link} hospedado por %{osm_france_link}
       hotosm_name: Equipa Humanitária do OpenStreetMap
     site:
@@ -3122,7 +3165,7 @@ pt-PT:
         graphhopper_car: Carro (GraphHopper)
         graphhopper_foot: A pé (GraphHopper)
         fossgis_valhalla_bicycle: Bicicleta (Valhalla)
-        fossgis_valhalla_car: Automóvel (Valhalla)
+        fossgis_valhalla_car: Carro (Valhalla)
         fossgis_valhalla_foot: Pé (Valhalla)
       descend: Descida
       directions: Direções
index 5a46056f7de324e7d1dfd25e0c6d7ecb953cbfd6..cc27269ba33d1793b68c4c373c87e4cf041edad9 100644 (file)
@@ -636,11 +636,35 @@ pt:
       newer_comments: Comentários mais recentes
       older_comments: Comentários mais antigos
   doorkeeper:
+    errors:
+      messages:
+        account_selection_required: O servidor de autorização requer a seleção da
+          conta do usuário final
+        consent_required: O servidor de autorização requer o consentimento do usuário
+          final
+        interaction_required: O servidor de autorização requer interação do usuário
+          final
+        login_required: O servidor de autorização requer a autenticação do usuário
+          final
     flash:
       applications:
         create:
           notice: Aplicação registada.
+    openid_connect:
+      errors:
+        messages:
+          auth_time_from_resource_owner_not_configured: Falha devido a falta de configuração
+            de Doorkeeper::OpenidConnect.configure.auth_time_from_resource_owner.
+          reauthenticate_resource_owner_not_configured: Falha devido a falta de configuração
+            de Doorkeeper::OpenidConnect.configure.reauthenticate_resource_owner.
+          resource_owner_from_access_token_not_configured: Falha devido a falta de
+            configuração de Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token.
+          select_account_for_resource_owner_not_configured: Falha devido a falta de
+            configuração de Doorkeeper::OpenidConnect.configure.select_account_for_resource_owner.
+          subject_not_configured: A geração de tokens de identificação falhou devido
+            a falta de configuração de Doorkeeper::OpenidConnect.configure.subject.
     scopes:
+      address: Ver seu endereço físico
       email: Ver seu endereço de e-mail
       openid: Autenticar sua conta
       phone: Ver seu número de telefone
@@ -2102,6 +2126,12 @@ pt:
           de Recursos Naturais do Canadá) e StatCan (Divisão de Geografia, Estatísticas
           do Canadá).'
         contributors_ca_canada: Canadá
+        contributors_cz_credit_html: |-
+          %{czechia}: Contém dados da Administração Estatal de Agrimensura
+          e Cadastro licenciados sob %{cc_licence_link}
+        contributors_cz_czechia: Tcheca
+        contributors_cz_cc_licence: Atribuição Creative Commons 4.0 Licença Internacional
+          (CC BY 4.0)
         contributors_fi_credit_html: '%{finland}: Contém dados do Serviço Topográfico
           Nacional da Base de Dados Topográfica da Finlândia e outros conjuntos de
           dados, sob a %{nlsfi_license_link}.'
@@ -2626,6 +2656,7 @@ pt:
     permissions:
       missing: Você não permitiu o acesso da aplicação a esta facilidade
     scopes:
+      openid: Iniciar sessão usando OpenStreetMap
       read_prefs: Ler preferências de usuário
       write_prefs: Modificar preferências de usuário
       write_diary: Criar entradas de diário, comentar e fazer amigos
@@ -2827,12 +2858,15 @@ pt:
       role:
         administrator: Este usuário é um administrador
         moderator: Este usuário é um moderador
+        importer: Este usuário é importador
         grant:
           administrator: Conceder acesso de administrador
           moderator: Conceder acesso de moderador
+          importer: Conceder o acesso de importador
         revoke:
           administrator: Revogar acesso de administrador
           moderator: Revogar acesso de moderador
+          importer: Revogar o acesso de importador
       block_history: Bloqueios ativos
       moderator_history: Bloqueios aplicados
       comments: Comentários
@@ -3139,7 +3173,7 @@ pt:
     edit_help: Mover o mapa e ampliar uma localização que pretende editar e clique
       aqui.
     directions:
-      ascend: Ascender
+      ascend: Ascenção
       engines:
         fossgis_osrm_bike: Bicicleta (OSRM)
         fossgis_osrm_car: Carro (OSRM)
@@ -3149,8 +3183,8 @@ pt:
         graphhopper_foot: A pé (GraphHopper)
         fossgis_valhalla_bicycle: Bicicleta (Valhalla)
         fossgis_valhalla_car: Carro (Valhalla)
-        fossgis_valhalla_foot: Pé (Valhalla)
-      descend: Descender
+        fossgis_valhalla_foot: A pé (Valhalla)
+      descend: Descida
       directions: Itinerário
       distance: Distância
       distance_m: '%{distance}m'
@@ -3159,63 +3193,63 @@ pt:
         no_route: Rota entre esses dois lugares não encontrada.
         no_place: Desculpe - não foi possível encontrar '%{place}'.
       instructions:
-        continue_without_exit: Continuar em %{name}
+        continue_without_exit: Continue em %{name}
         slight_right_without_exit: Curva suave à direita para %{name}
-        offramp_right: Conduza até a rampa do lado direito
-        offramp_right_with_exit: Pegue a saída %{exit} a direita
-        offramp_right_with_exit_name: Pegue a saída %{exit} à direita na %{name}
-        offramp_right_with_exit_directions: Pegue a saída %{exit} à direita para %{directions}
-        offramp_right_with_exit_name_directions: Pegue a saída %{exit} à direita na
-          %{name}, em direção %{directions}
-        offramp_right_with_name: Pegue a via de acesso à direita na %{name}
-        offramp_right_with_directions: Conduza até a rampa do lado direito em direção
+        offramp_right: Pegue a rampa à direita
+        offramp_right_with_exit: Pegue a saída %{exit} à direita
+        offramp_right_with_exit_name: Pegue a saída %{exit} à direita para %{name}
+        offramp_right_with_exit_directions: Pegue a saída %{exit} à direita em direção
           a %{directions}
-        offramp_right_with_name_directions: Conduza até a rampa do lado direito para
+        offramp_right_with_exit_name_directions: Pegue a saída %{exit} à direita para
           %{name}, em direção a %{directions}
-        onramp_right_without_exit: Vire à direita, na via de acesso, na %{name}
+        offramp_right_with_name: Pegue a rampa à direita para %{name}
+        offramp_right_with_directions: Pegue a rampa à direita em direção a %{directions}
+        offramp_right_with_name_directions: Pegue a rampa à direita para %{name},
+          em direção a %{directions}
+        onramp_right_without_exit: Vire à direita na rampa para %{name}
         onramp_right_with_directions: Vire à direita na rampa em direção a %{directions}
         onramp_right_with_name_directions: Vire à direita na rampa para %{name}, em
           direção a %{directions}
-        onramp_right_without_directions: Vire à direita na rampa
+        onramp_right_without_directions: Vire à direita para a rampa
         onramp_right: Vire à direita para a rampa
-        endofroad_right_without_exit: No fim da estrada, vire à direita na %{name}
-        merge_right_without_exit: Entre à direita na %{name}
-        fork_right_without_exit: Na bifurcação, vire à direita na %{name}
+        endofroad_right_without_exit: No fim da estrada, vire à direita para %{name}
+        merge_right_without_exit: Entre à direita para %{name}
+        fork_right_without_exit: Na bifurcação, vire à direita para %{name}
         turn_right_without_exit: Vire à direita para %{name}
         sharp_right_without_exit: Curva acentuada à direita para %{name}
         uturn_without_exit: Retorno em %{name}
         sharp_left_without_exit: Curva acentuada à esquerda para %{name}
         turn_left_without_exit: Vire à esquerda para %{name}
-        offramp_left: Conduza até a rampa do lado esquerdo
+        offramp_left: Pegue a rampa à esquerda
         offramp_left_with_exit: Pegue a saída %{exit} à esquerda
-        offramp_left_with_exit_name: Pegue a saída %{exit} à esquerda na %{name}
-        offramp_left_with_exit_directions: Pegue a saída %{exit} à esquerda para %{directions}
-        offramp_left_with_exit_name_directions: Pegue a saída %{exit} à esquerda na
-          %{name}, em direção %{directions}
-        offramp_left_with_name: Pegue a via de acesso à esquerda na %{name}
-        offramp_left_with_directions: Conduza até a rampa do lado esquerdo em direção
+        offramp_left_with_exit_name: Pegue a saída %{exit} à esquerda para %{name}
+        offramp_left_with_exit_directions: Pegue a saída %{exit} à esquerda em direção
           a %{directions}
-        offramp_left_with_name_directions: Conduza até a tampa do lado esquerdo para
+        offramp_left_with_exit_name_directions: Pegue a saída %{exit} à esquerda para
           %{name}, em direção a %{directions}
-        onramp_left_without_exit: Vire à esquerda, na via de acesso, na %{name}
+        offramp_left_with_name: Pegue a rampa à esquerda para %{name}
+        offramp_left_with_directions: Pegue a rampa à esquerda em direção a %{directions}
+        offramp_left_with_name_directions: Pegue a rampa à esquerda para %{name},
+          em direção a %{directions}
+        onramp_left_without_exit: Vire à esquerda na rampa para %{name}
         onramp_left_with_directions: Vire à esquerda na rampa em direção a %{directions}
         onramp_left_with_name_directions: Vire à esquerda na rampa para %{name}, em
           direção a %{directions}
-        onramp_left_without_directions: Vire à esquerda na rampa
+        onramp_left_without_directions: Vire à esquerda para a rampa
         onramp_left: Vire à esquerda para a rampa
-        endofroad_left_without_exit: No fim da estrada, vire à esquerda na %{name}
-        merge_left_without_exit: Entre à esquerda na %{name}
-        fork_left_without_exit: Na bifurcação, vire à esquerda na %{name}
-        slight_left_without_exit: Esquerda suave para %{name}
-        via_point_without_exit: (ponto de passagem)
+        endofroad_left_without_exit: No fim da estrada, vire à esquerda para %{name}
+        merge_left_without_exit: Entre à esquerda para %{name}
+        fork_left_without_exit: Na bifurcação, vire à esquerda para %{name}
+        slight_left_without_exit: Curva suave à esquerda para %{name}
+        via_point_without_exit: (ponto intermediário)
         follow_without_exit: Siga %{name}
         roundabout_without_exit: Na rotatória, pegue a saída para %{name}
         leave_roundabout_without_exit: Saia da rotatória - %{name}
         stay_roundabout_without_exit: Mantenha-se na rotatória - %{name}
         start_without_exit: Comece em %{name}
-        destination_without_exit: Chegue ao destino
-        against_oneway_without_exit: Vá contra o sentido da mão única em %{name}
-        end_oneway_without_exit: Final de mão única em %{name}
+        destination_without_exit: Chegada ao destino
+        against_oneway_without_exit: Vá na contra-mão em %{name}
+        end_oneway_without_exit: Fim de mão única em %{name}
         roundabout_with_exit: Na rotatória, pegue a saída %{exit} para %{name}
         roundabout_with_exit_ordinal: Na rotatória, pegue %{exit} saída para %{name}
         exit_roundabout: Saia da rotatória para %{name}
@@ -3241,8 +3275,8 @@ pt:
       error: 'Erro ao contatar %{server}: %{error}'
       timeout: Tempo esgotado com %{server}
     context:
-      directions_from: Início da rota
-      directions_to: Destino da rota
+      directions_from: Início de rota a partir daqui
+      directions_to: Fim de rota até daqui
       add_note: Adicionar uma nota aqui
       show_address: Mostrar endereço
       query_features: Consultar elementos
@@ -3254,9 +3288,9 @@ pt:
     index:
       empty: Nenhuma anulação para mostrar.
       heading: Lista de anulações
-      title: Lista de redações
+      title: Lista de anulações
     new:
-      heading: Digite informações para a nova anulação
+      heading: Introduza a informação da nova anulação
       title: Criando uma nova anulação
     show:
       description: 'Descrição:'
@@ -3264,7 +3298,7 @@ pt:
       title: Exibindo anulação
       user: 'Criador:'
       edit: Editar esta anulação
-      destroy: Remover esta redação
+      destroy: Remover esta anulação
       confirm: Tem certeza?
     create:
       flash: Anulação criada.
@@ -3273,8 +3307,8 @@ pt:
     destroy:
       not_empty: A anulação não está vazia. Desanule todas as versões pertencentes
         a esta anulação antes de destruí-la.
-      flash: Redação destruída.
-      error: Houve um erro ao destruir esta anulação.
+      flash: Anulação eliminada.
+      error: Houve um erro ao tentar eliminar esta anulação.
   validations:
     leading_whitespace: tem espaço em branco principal
     trailing_whitespace: tem espaço em branco à direita
index 00db6a8de686abcac9c2ef926bc3c76c5a451c2a..55c84d5333eaaeeff3dc4b16749e56d3e0a44c6f 100644 (file)
@@ -901,7 +901,7 @@ ru:
           college: Здание колледжа
           commercial: Офисное здание
           construction: Строящееся здание
-          detached: Ð\9eÑ\81обнÑ\8fк
+          detached: Ð\9eÑ\82делÑ\8cноÑ\81Ñ\82оÑ\8fÑ\89ий Ð¶Ð¸Ð»Ð¾Ð¹ Ð´Ð¾Ð¼
           dormitory: Общежитие
           duplex: Дуплекс
           farm: Ферма
@@ -921,7 +921,7 @@ ru:
           office: Офисное здание
           public: Общественное здание
           residential: Жилой дом
-          retail: Ð\97дание Ð½Ð° Ð¿Ñ\80одажÑ\83
+          retail: Ð¢Ð¾Ñ\80говое Ð·Ð´Ð°Ð½Ð¸Ðµ
           roof: Крыша
           ruins: Разрушенное здание
           school: Здание школы
@@ -931,7 +931,7 @@ ru:
           stable: Конюшня
           static_caravan: Передвижной дом
           temple: Здание храма
-          terrace: Ð\97дание Ñ\81 Ñ\82еÑ\80Ñ\80аÑ\81ой
+          terrace: Ð Ñ\8fд Ð´Ð¾Ð¼Ð¾Ð²
           train_station: Железнодорожный вокзал
           university: Университет
           warehouse: Склад
index 04f5d2797b2741074d63cd335900ca0980849c54..791d63e93850fb37d9d13bf329c00ee63976f2ee 100644 (file)
@@ -346,7 +346,12 @@ sv:
         kan inte skicka meddelanden till dig eller se din plats. För att visa vad
         du redigerade och tillåta andra att kontakta dig via webbplatsen, klicka på
         knappen nedan.
+      only_public_can_edit: Sedan övergången till 0.6-API:n kan bara offentliga användare
+        redigera kartdata.
       find_out_why: ta reda på varför
+      email_not_revealed: Din e-postadress kommer inte avslöjas när du blir offentlig.
+      not_reversible: Denna åtgärd kan inte ångras och alla nya användare är nu offentliga
+        som standard.
       make_edits_public_button: Gör alla mina redigeringar offentliga
     update:
       success_confirm_needed: Användarinformation uppdaterades. Kontrollera din e-post
@@ -654,6 +659,8 @@ sv:
     contact:
       contact_url_title: Olika kontaktkanaler förklaras
       contact: kontakta
+      contact_the_community_html: Gå gärna till %{contact_link} OpenStreetMap-gemenskapen
+        om du har hittat en trasig länk/bugg. Anteckna din förfrågas exakta webbadress.
     forbidden:
       title: Förbjudet
       description: Åtgärden du begärde på OpenStreetMap-servern är endast tillgänglig
@@ -2031,10 +2038,18 @@ sv:
           OpenStreetMap%{registered_trademark_link} är %{open_data}, licensierad under
           %{odc_odbl_link} (ODbL) av %{osm_foundation_link} (OSMF).
         introduction_1_open_data: öppna data
+        introduction_1_odc_odbl: Open Data Commons Open Database License
         introduction_1_osm_foundation: OpenStreetMap-stiftelsen
+        introduction_2_html: |-
+          Du är fri att kopiera, distribuera, överföra och anpassa vår data,
+          så länge du anger OpenStreetMap och dess bidragsgivare som källa.
+          Om du ändrar eller bygger vidare på vår data kan du
+          endast distribuera resultatet under samma licens. Den
+          fullständiga %{legal_code_link} förklarar dina rättigheter och skyldigheter.
         introduction_2_legal_code: juridiska texten
         introduction_3_html: Vår dokumentation är licensierad under licensen %{creative_commons_link}
           (CC BY-SA 2.0).
+        introduction_3_creative_commons: Creative Commons Erkännande-DelaLika 2.0
         credit_title_html: Hur du anger OpenStreetMap som källa
         credit_1_html: 'När du använder OpenStreetMap-data, måste du göra dessa två
           saker:'
@@ -2045,6 +2060,7 @@ sv:
           regler för hur upphovsrättsmeddelandet ska visas beroende på om du har
           skapat en bläddringsbar karta, en utskriven karta eller en statisk bild. Fullständig information om
           -kraven finns i %{attribution_guidelines_link}.
+        credit_3_attribution_guidelines: Riktlinjer för tillskrivning
         credit_4_1_html: "För att tydliggöra att data är tillgänglig under Öppen\nDatabase
           Licensen kan du länka till %{this_copyright_page_link}.\nAlternativt, och
           som ett krav om du distribuerar OSM i en\ndataform kan du namn och länk
@@ -2076,7 +2092,9 @@ sv:
         contributors_at_stadt_wien: Staden Wien
         contributors_at_cc_by: CC BY
         contributors_at_land_vorarlberg: Förbundsland Vorarlberg
+        contributors_at_cc_by_at_with_amendments: CC BY AT med tillägg
         contributors_au_australia: Australien
+        contributors_au_geoscape_australia: Geoscape Australien
         contributors_ca_canada: Kanada
         contributors_cz_czechia: Tjeckien
         contributors_fi_finland: Finland
@@ -2166,6 +2184,7 @@ sv:
       embeddable_html: Inbäddad HTML
       licence: Licens
       licence_details_html: OpenStreetMap-data licensieras under %{odbl_link} (ODbL).
+      odbl: Open Data Commons Open Database License
       too_large:
         advice: 'Om ovanstående export misslyckas, vänligen överväg att använda en
           av de källor som anges nedan:'
@@ -2389,6 +2408,7 @@ sv:
         editor: redigerare
         node: nod
         way: sträcka
+        tag: tagg
       rules:
         title: Regler!
         para_1_html: |-
@@ -2785,12 +2805,15 @@ sv:
       role:
         administrator: Den här användaren är en administratör
         moderator: Den här användaren är en moderator
+        importer: Denna användare är en importör
         grant:
           administrator: Tilldela administratörsrättigheter
           moderator: Tilldela moderatorrättigheter
+          importer: Ge importåtkomst
         revoke:
           administrator: Återkalla administratörsrättigheter
           moderator: Återkalla moderatorrättigheter
+          importer: Återkalla importåtkomst
       block_history: Aktiva blockeringar
       moderator_history: Utdelade blockeringar
       comments: Kommentarer
index ee0960e94797773d7a530bda675e4be48d12893b..e6be76e3db440f4218dc937fd1a491536bfd8c3f 100644 (file)
@@ -2602,12 +2602,15 @@ zh-TW:
       role:
         administrator: 這個使用者是一個管理員
         moderator: 這個使用者是一個仲裁員
+        importer: 此名使用者是匯入者
         grant:
           administrator: 授予管理員權限
           moderator: 授予仲裁員權限
+          importer: 授予匯入權限
         revoke:
           administrator: 撤銷管理員權限
           moderator: 撤銷仲裁員權限
+          importer: 撤銷匯入權限
       block_history: 已封鎖
       moderator_history: 給予封鎖
       comments: 評論
index 87c467c88301b11970ea43e365134f58f153bb0a..1c9c7e0a1123481543d2f2950b733fee71f5aeac 100644 (file)
@@ -53,6 +53,8 @@ api_timeout: 300
 web_timeout: 30
 # Periods (in hours) which are allowed for user blocks
 user_block_periods: [0, 1, 3, 6, 12, 24, 48, 96, 168, 336, 731, 4383, 8766, 87660]
+# Account deletion cooldown period (in hours) since last changeset close; null to disable, 0 to make sure there aren't any open changesets when the deletion happens
+user_account_deletion_delay: null
 # Rate limit for message sending
 max_messages_per_hour: 60
 # Rate limit for friending
diff --git a/db/migrate/20231117170422_add_closed_at_index_to_changesets.rb b/db/migrate/20231117170422_add_closed_at_index_to_changesets.rb
new file mode 100644 (file)
index 0000000..e9d7e62
--- /dev/null
@@ -0,0 +1,7 @@
+class AddClosedAtIndexToChangesets < ActiveRecord::Migration[7.1]
+  disable_ddl_transaction!
+
+  def change
+    add_index :changesets, [:user_id, :closed_at], :algorithm => :concurrently
+  end
+end
index 56e778523f7a7597d3a26670cc0dae97187a57e7..f74d4d571e2f58c8d3ef65009af3345d6128ea7b 100644 (file)
@@ -2499,6 +2499,13 @@ CREATE INDEX index_changeset_comments_on_changeset_id_and_created_at ON public.c
 CREATE INDEX index_changeset_comments_on_created_at ON public.changeset_comments USING btree (created_at);
 
 
+--
+-- Name: index_changesets_on_user_id_and_closed_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_changesets_on_user_id_and_closed_at ON public.changesets USING btree (user_id, closed_at);
+
+
 --
 -- Name: index_changesets_subscribers_on_changeset_id; Type: INDEX; Schema: public; Owner: -
 --
@@ -3499,6 +3506,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('23'),
 ('22'),
 ('21'),
+('20231117170422'),
 ('20231101222146'),
 ('20231029151516'),
 ('20231010194809'),
index 7546c3797ee6f2939834dd1d9a0fc1b68587f6ba..131292f412d90f286b8798b81b84bde1015b2be4 100644 (file)
@@ -152,4 +152,23 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
     # Make sure we have a button to "go public"
     assert_select "form.button_to[action='/user/go_public']", true
   end
+
+  def test_destroy_allowed
+    user = create(:user)
+    session_for(user)
+
+    delete account_path
+    assert_response :redirect
+  end
+
+  def test_destroy_not_allowed
+    with_user_account_deletion_delay(24) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc)
+      session_for(user)
+
+      delete account_path
+      assert_response :bad_request
+    end
+  end
 end
index a4ed07e09eac0079a610f8e03762b0b89aeda105..5c48bb9698a6a73d56b7ab120b871d24902d18c5 100644 (file)
@@ -282,4 +282,62 @@ class UserTest < ActiveSupport::TestCase
     oauth_access_token.reload
     assert_predicate oauth_access_token, :revoked?
   end
+
+  def test_deletion_allowed_when_no_changesets
+    with_user_account_deletion_delay(10000) do
+      user = create(:user)
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_without_delay
+    with_user_account_deletion_delay(nil) do
+      user = create(:user)
+      create(:changeset, :user => user)
+      user.reload
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_past_delay
+    with_user_account_deletion_delay(10) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc - 12.hours, :closed_at => Time.now.utc - 10.hours)
+      user.reload
+      assert_predicate user, :deletion_allowed?
+    end
+  end
+
+  def test_deletion_allowed_during_delay
+    with_user_account_deletion_delay(10) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc - 11.hours, :closed_at => Time.now.utc - 9.hours)
+      user.reload
+      assert_not_predicate user, :deletion_allowed?
+      assert_equal Time.now.utc + 1.hour, user.deletion_allowed_at
+    end
+  end
+
+  def test_deletion_allowed_past_zero_delay
+    with_user_account_deletion_delay(0) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+      travel 90.minutes do
+        user.reload
+        assert_predicate user, :deletion_allowed?
+      end
+    end
+  end
+
+  def test_deletion_allowed_during_zero_delay
+    with_user_account_deletion_delay(0) do
+      user = create(:user)
+      create(:changeset, :user => user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+      travel 30.minutes do
+        user.reload
+        assert_not_predicate user, :deletion_allowed?
+        assert_equal Time.now.utc + 30.minutes, user.deletion_allowed_at
+      end
+    end
+  end
 end
index 87e981c6426500443bcbf6811cef581f8ffce9b1..e6517dccc38b6bd0c5b17bb3463c5e34111bfdd3 100644 (file)
@@ -41,4 +41,59 @@ class AccountDeletionTest < ApplicationSystemTestCase
 
     assert_content "Account Deleted"
   end
+
+  test "can delete with any delay setting value if the user has no changesets" do
+    with_user_account_deletion_delay(10000) do
+      travel 1.hour do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can delete with delay disabled" do
+    with_user_account_deletion_delay(nil) do
+      create(:changeset, :user => @user)
+
+      travel 1.hour do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can delete when last changeset is old enough" do
+    with_user_account_deletion_delay(10) do
+      create(:changeset, :user => @user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+
+      travel 12.hours do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_no_content "cannot currently be deleted"
+      end
+    end
+  end
+
+  test "can't delete when last changeset isn't old enough" do
+    with_user_account_deletion_delay(10) do
+      create(:changeset, :user => @user, :created_at => Time.now.utc, :closed_at => Time.now.utc + 1.hour)
+
+      travel 10.hours do
+        visit edit_account_path
+
+        click_link "Delete Account..."
+
+        assert_content "cannot currently be deleted"
+        assert_content "in about 1 hour"
+      end
+    end
+  end
 end
index 68749c0f70f595e00b322b21acee0215b7d46b51..19e1a2784611fa219af508a181909c7ead6f320c 100644 (file)
@@ -371,5 +371,16 @@ module ActiveSupport
         el << tag_el
       end
     end
+
+    def with_user_account_deletion_delay(value)
+      freeze_time
+      default_value = Settings.user_account_deletion_delay
+      Settings.user_account_deletion_delay = value
+
+      yield
+
+      Settings.user_account_deletion_delay = default_value
+      unfreeze_time
+    end
   end
 end