Skip to content

Commit f46fd62

Browse files
authored
Merge pull request #413 from gtt-project/feat/watch-radius-unit
feat: nearby-watch radius in the configured display unit
2 parents dcaa6be + 333b22e commit f46fd62

9 files changed

Lines changed: 118 additions & 44 deletions

File tree

app/views/redmine_gtt/hooks/_watch_nearby_preference.html.erb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@
1919
</p>
2020
<p>
2121
<label for="pref_gtt_watch_radius"><%= l(:gtt_label_watch_nearby_radius) %></label>
22-
<%= number_field_tag 'pref[gtt_watch_radius]', user.pref.gtt_watch_radius,
23-
id: 'pref_gtt_watch_radius', min: 1,
24-
max: RedmineGtt::Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_KM,
25-
step: 1, disabled: !location_set %>
26-
<%= l(:gtt_label_watch_nearby_radius_unit) %>
22+
<%# entered in the configured display unit, stored in meters (#10) %>
23+
<%# min = 1 meter expressed in the unit: the server treats smaller
24+
values as unset, so don't offer them %>
25+
<%= number_field_tag 'pref[gtt_watch_radius_in_unit]', user.pref.gtt_watch_radius_in_unit,
26+
id: 'pref_gtt_watch_radius', step: 'any',
27+
min: RedmineGtt::DistanceUnit.from_meters(1).round(6),
28+
max: RedmineGtt::DistanceUnit.from_meters(
29+
RedmineGtt::Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_M
30+
).ceil,
31+
disabled: !location_set %>
32+
<%= RedmineGtt::DistanceUnit.current %>
2733
</p>
2834
<% unless location_set %>
2935
<p><em class="info"><%= l(:gtt_text_watch_nearby_requires_location) %></em></p>

config/locales/de.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,4 @@ de:
142142
gtt_label_watch_nearby_legend: "Automatisch beobachten: Tickets in der Nähe"
143143
gtt_label_watch_nearby: "Neue Tickets in der Nähe meines Standorts beobachten"
144144
gtt_label_watch_nearby_radius: "Entfernung"
145-
gtt_label_watch_nearby_radius_unit: "km"
146145
gtt_text_watch_nearby_requires_location: "Legen Sie zuerst Ihren Standort auf der Karte oben fest, um dies zu aktivieren."

config/locales/en.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ en:
7777
gtt_label_watch_nearby_legend: "Auto watch: nearby issues"
7878
gtt_label_watch_nearby: "Watch new issues near my location"
7979
gtt_label_watch_nearby_radius: "Distance"
80-
gtt_label_watch_nearby_radius_unit: "km"
8180
gtt_text_watch_nearby_requires_location: "Set your location on the map above to enable this."
8281

8382
select_default_tracker_icon: "Select default tracker icon:"

config/locales/ja.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ ja:
7474
gtt_label_watch_nearby_legend: "自動ウォッチ: 近くのチケット"
7575
gtt_label_watch_nearby: "登録した所在地の近くの新しいチケットをウォッチする"
7676
gtt_label_watch_nearby_radius: "距離"
77-
gtt_label_watch_nearby_radius_unit: "km"
7877
gtt_text_watch_nearby_requires_location: "有効にするには上の地図で所在地を設定してください。"
7978

8079
select_default_tracker_icon: "トラッカーアイコンを選択:"

lib/redmine_gtt/nearby_watchers.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def self.subscribe(issue)
1919

2020
def self.nearby_users(issue)
2121
candidates(issue).select do |user|
22-
radius_km = user.pref.gtt_watch_nearby? && user.pref.gtt_watch_radius_km
23-
radius_km &&
24-
user['gtt_distance_m'].to_f <= radius_km * 1000 &&
22+
radius_m = user.pref.gtt_watch_nearby? && user.pref.gtt_watch_radius_m
23+
radius_m &&
24+
user['gtt_distance_m'].to_f <= radius_m &&
2525
issue.visible?(user)
2626
end
2727
end
@@ -30,7 +30,7 @@ def self.nearby_users(issue)
3030
# The issue geometry is bound as an EWKB hex parameter; the distance is
3131
# measured on the geography type (meters, any geometry type).
3232
def self.candidates(issue)
33-
max_meters = Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_KM * 1000
33+
max_meters = Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_M
3434
distance = User.send(:sanitize_sql_array, [
3535
"ST_Distance(#{User.table_name}.geom::geography, ?::geometry::geography)",
3636
ewkb_hex(issue.geom)

lib/redmine_gtt/patches/user_preference_patch.rb

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ module Patches
44
# Stores the "auto watch nearby issues" opt-in (#14) in the serialized
55
# UserPreference#others hash, so no schema change is needed. The user's
66
# stored location (users.geom) is the center of the watch area; the
7-
# radius is kept in kilometers as entered on the My account page.
7+
# radius is stored in meters (the plugin's internal unit, #10) and
8+
# entered/displayed in the configured display unit on My account.
89
#
910
# This patch only adds the preference storage and validated readers.
1011
# The watcher assignment itself hooks into issue creation separately.
1112
module UserPreferencePatch
1213

13-
# Upper bound for the watch radius, enforced server-side (the form's
14-
# max attribute mirrors it, but client-side limits are easy to bypass)
15-
# so an arbitrarily large radius can't make the watcher query expensive.
16-
NEARBY_WATCH_MAX_RADIUS_KM = 1000
14+
# Upper bound for the watch radius (1000 km), enforced server-side
15+
# (the form's max attribute mirrors it, but client-side limits are
16+
# easy to bypass) so an arbitrarily large radius can't make the
17+
# watcher query expensive.
18+
NEARBY_WATCH_MAX_RADIUS_M = 1_000_000
1719

1820
def self.apply
1921
unless UserPreference < self
2022
UserPreference.prepend self
21-
UserPreference.safe_attributes 'gtt_watch_nearby', 'gtt_watch_radius'
23+
UserPreference.safe_attributes 'gtt_watch_nearby', 'gtt_watch_radius_in_unit'
2224
end
2325
end
2426

@@ -28,6 +30,8 @@ def self.apply
2830
def gtt_watch_nearby; self[:gtt_watch_nearby]; end
2931
def gtt_watch_nearby=(value); self[:gtt_watch_nearby] = value; end
3032

33+
# Raw stored radius (meters). Not mass-assignable; the account form
34+
# goes through gtt_watch_radius_in_unit.
3135
def gtt_watch_radius; self[:gtt_watch_radius]; end
3236
def gtt_watch_radius=(value); self[:gtt_watch_radius] = value; end
3337

@@ -36,12 +40,37 @@ def gtt_watch_nearby?
3640
gtt_watch_nearby.to_s == '1'
3741
end
3842

39-
# Validated radius in kilometers: a positive Integer capped at
40-
# NEARBY_WATCH_MAX_RADIUS_KM, or nil when the preference is unset or
43+
# Validated radius in meters: a positive number capped at
44+
# NEARBY_WATCH_MAX_RADIUS_M, or nil when the preference is unset or
4145
# holds a non-numeric/non-positive value.
42-
def gtt_watch_radius_km
43-
value = Integer(gtt_watch_radius.to_s, exception: false)
44-
value.clamp(1, NEARBY_WATCH_MAX_RADIUS_KM) if value&.positive?
46+
def gtt_watch_radius_m
47+
value = Float(gtt_watch_radius.to_s, exception: false)
48+
[value, NEARBY_WATCH_MAX_RADIUS_M].min if value&.positive?
49+
end
50+
51+
# Form-facing virtual attribute: the radius in the configured display
52+
# unit. Whole numbers render without a decimal part; fractions keep
53+
# 6 decimals (enough for 1 m in any supported unit), so a read->save
54+
# round trip cannot drift the stored meters.
55+
def gtt_watch_radius_in_unit
56+
meters = gtt_watch_radius_m
57+
return nil unless meters
58+
59+
value = DistanceUnit.from_meters(meters)
60+
value == value.to_i ? value.to_i : value.round(6)
61+
end
62+
63+
def gtt_watch_radius_in_unit=(value)
64+
stripped = value.to_s.strip
65+
self.gtt_watch_radius =
66+
if stripped.empty?
67+
nil
68+
elsif (number = Float(stripped, exception: false))
69+
DistanceUnit.to_meters(number).round.to_s
70+
else
71+
# keep the garbage; gtt_watch_radius_m rejects it as before
72+
stripped
73+
end
4574
end
4675

4776
end

test/functional/my_account_watch_nearby_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ class MyAccountWatchNearbyTest < Redmine::ControllerTest
3030
end
3131

3232
test 'saving other settings without a location leaves the preference untouched' do
33-
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25')
33+
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25000')
3434

3535
put :account, params: { user: { firstname: 'Dave' }, pref: { no_self_notified: '1' } }
3636

3737
assert_redirected_to '/my/account'
3838
pref = User.find(@user.id).pref
3939
assert pref.gtt_watch_nearby?
40-
assert_equal 25, pref.gtt_watch_radius_km
40+
assert_equal 25_000, pref.gtt_watch_radius_m
4141
end
4242

4343
test 'renders the fieldset enabled once a location is stored' do
@@ -56,18 +56,18 @@ class MyAccountWatchNearbyTest < Redmine::ControllerTest
5656

5757
put :account, params: {
5858
user: { firstname: @user.firstname },
59-
pref: { gtt_watch_nearby: '1', gtt_watch_radius: '25' }
59+
pref: { gtt_watch_nearby: '1', gtt_watch_radius_in_unit: '25' }
6060
}
6161

6262
assert_redirected_to '/my/account'
6363
pref = User.find(@user.id).pref
6464
assert pref.gtt_watch_nearby?
65-
assert_equal 25, pref.gtt_watch_radius_km
65+
assert_equal 25, pref.gtt_watch_radius_m
6666
end
6767

6868
test 'unchecking the box turns the preference off' do
6969
@user.update_attribute :geojson, example_geojson
70-
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25')
70+
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25000')
7171

7272
put :account, params: {
7373
user: { firstname: @user.firstname },
@@ -78,6 +78,6 @@ class MyAccountWatchNearbyTest < Redmine::ControllerTest
7878
pref = User.find(@user.id).pref
7979
assert_not pref.gtt_watch_nearby?
8080
# the radius survives so re-enabling does not lose the value
81-
assert_equal 25, pref.gtt_watch_radius_km
81+
assert_equal 25_000, pref.gtt_watch_radius_m
8282
end
8383
end

test/unit/nearby_watchers_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class NearbyWatchersTest < GttTest
1212
setup do
1313
@user = User.find_by_login 'dlopper' # member of the public project 1
1414
@user.update_attribute :geojson, point_geojson(TOKYO)
15-
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25')
15+
@user.pref.update(gtt_watch_nearby: '1', gtt_watch_radius: '25000')
1616
end
1717

1818
test 'subscribes an opted-in user within their radius on issue creation' do
@@ -31,7 +31,7 @@ class NearbyWatchersTest < GttTest
3131

3232
test 'subscribes a distant user whose radius is large enough' do
3333
@user.update_attribute :geojson, point_geojson(OSAKA)
34-
@user.pref.update(gtt_watch_radius: '500')
34+
@user.pref.update(gtt_watch_radius: '500000')
3535

3636
issue = create_issue_with_geom!(1, NEAR_TOKYO)
3737

test/unit/user_preference_patch_test.rb

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,41 @@ class UserPreferencePatchTest < GttTest
77
@pref = User.find_by_login('dlopper').pref
88
end
99

10+
teardown do
11+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
12+
'distance_unit' => 'm'
13+
)
14+
end
15+
1016
test 'preference is off by default' do
1117
assert_not @pref.gtt_watch_nearby?
12-
assert_nil @pref.gtt_watch_radius_km
18+
assert_nil @pref.gtt_watch_radius_m
1319
end
1420

1521
test 'accessors persist through the serialized others hash' do
1622
@pref.gtt_watch_nearby = '1'
17-
@pref.gtt_watch_radius = '25'
23+
@pref.gtt_watch_radius = '25000'
1824
assert @pref.save
1925

2026
pref = User.find_by_login('dlopper').pref
2127
assert pref.gtt_watch_nearby?
22-
assert_equal 25, pref.gtt_watch_radius_km
28+
assert_equal 25_000, pref.gtt_watch_radius_m
2329
end
2430

25-
test 'safe_attributes mass-assignment works for the new keys' do
31+
test 'safe_attributes mass-assignment works for the form keys' do
2632
@pref.safe_attributes = {
27-
'gtt_watch_nearby' => '1', 'gtt_watch_radius' => '10'
33+
'gtt_watch_nearby' => '1', 'gtt_watch_radius_in_unit' => '10'
2834
}
2935
assert @pref.save
3036

3137
pref = User.find_by_login('dlopper').pref
3238
assert pref.gtt_watch_nearby?
33-
assert_equal 10, pref.gtt_watch_radius_km
39+
assert_equal 10, pref.gtt_watch_radius_m
40+
end
41+
42+
test 'the raw radius is not mass-assignable' do
43+
@pref.safe_attributes = { 'gtt_watch_radius' => '123' }
44+
assert_nil @pref.gtt_watch_radius_m
3445
end
3546

3647
test 'gtt_watch_nearby? treats anything but "1" as off' do
@@ -40,16 +51,47 @@ class UserPreferencePatchTest < GttTest
4051
end
4152
end
4253

43-
test 'gtt_watch_radius_km rejects blank, non-numeric and non-positive values' do
44-
[nil, '', 'abc', '0', '-5', '2.5'].each do |value|
54+
test 'gtt_watch_radius_m rejects blank, non-numeric and non-positive values' do
55+
[nil, '', 'abc', '0', '-5'].each do |value|
4556
@pref.gtt_watch_radius = value
46-
assert_nil @pref.gtt_watch_radius_km, "expected #{value.inspect} to be nil"
57+
assert_nil @pref.gtt_watch_radius_m, "expected #{value.inspect} to be nil"
4758
end
4859
end
4960

50-
test 'gtt_watch_radius_km caps the radius server-side' do
51-
@pref.gtt_watch_radius = '999999'
52-
assert_equal RedmineGtt::Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_KM,
53-
@pref.gtt_watch_radius_km
61+
test 'gtt_watch_radius_m caps the radius server-side' do
62+
@pref.gtt_watch_radius = '99999999'
63+
assert_equal RedmineGtt::Patches::UserPreferencePatch::NEARBY_WATCH_MAX_RADIUS_M,
64+
@pref.gtt_watch_radius_m
65+
end
66+
67+
test 'the form attribute converts through the configured display unit' do
68+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
69+
'distance_unit' => 'km'
70+
)
71+
@pref.gtt_watch_radius_in_unit = '25'
72+
assert_equal 25_000, @pref.gtt_watch_radius_m
73+
assert_equal 25, @pref.gtt_watch_radius_in_unit
74+
75+
@pref.gtt_watch_radius_in_unit = '0.5'
76+
assert_equal 500, @pref.gtt_watch_radius_m
77+
assert_equal 0.5, @pref.gtt_watch_radius_in_unit
78+
end
79+
80+
test 'a read-save round trip does not drift the stored meters' do
81+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
82+
'distance_unit' => 'mi'
83+
)
84+
@pref.gtt_watch_radius = '500' # meters, not a round mile value
85+
displayed = @pref.gtt_watch_radius_in_unit
86+
@pref.gtt_watch_radius_in_unit = displayed.to_s
87+
assert_equal 500, @pref.gtt_watch_radius_m
88+
end
89+
90+
test 'the form attribute keeps garbage rejected and blank clearing' do
91+
@pref.gtt_watch_radius_in_unit = 'abc'
92+
assert_nil @pref.gtt_watch_radius_m
93+
94+
@pref.gtt_watch_radius_in_unit = ' '
95+
assert_nil @pref.gtt_watch_radius
5496
end
5597
end

0 commit comments

Comments
 (0)