Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coordinate obscuration by day #2196

Merged
merged 29 commits into from Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c79e6ab
Ensure Observation.mappable compares ints with ints.
kueda Nov 15, 2018
35b12aa
Merge branch '2050-cache-taxon-geoprivacy' into 2037-coordinate-obscu…
kueda Nov 22, 2018
09b2ad7
First stab at coordinate obscuration by day (#2037)
kueda Nov 22, 2018
7a73d08
Merge branch '2029-user-trust' into 2037-coordinate-obscuration-by-day
kueda Nov 28, 2018
ac9009a
Fixed bug in which making coords private erased them from other obs.
kueda Nov 28, 2018
5dba09b
Sanity spec.
kueda Nov 29, 2018
ebbab02
Merge branch '2029-user-trust' into 2037-coordinate-obscuration-by-day
kueda Nov 29, 2018
bdbf669
Merge branch '2050-cache-taxon-geoprivacy' into 2037-coordinate-obscu…
kueda Jan 9, 2019
5af0e64
Show why coordinates are obscured on obs detail.
kueda Jan 10, 2019
7a698cc
Merge branch '2029-user-trust' into 2037-coordinate-obscuration-by-day
kueda Jan 15, 2019
6fb45fe
Merge branch '2029-user-trust' into 2037-coordinate-obscuration-by-day
kueda Jan 15, 2019
bbdabc5
Merge branch '2029-user-trust' into 2037-coordinate-obscuration-by-day
kueda Jan 15, 2019
10223be
Show reason coordinates are revealed when observer trusts viewer.
kueda Jan 15, 2019
7f5dcdb
Merge branch 'master' into 2037-coordinate-obscuration-by-day
kueda Jan 16, 2019
f3cd354
Merge branch 'master' into 2037-coordinate-obscuration-by-day
kueda Jan 30, 2019
4ff5f21
Made obscuration by day only apply to obs of threatened taxa by defau…
kueda Jan 30, 2019
8bfed29
Fixed spec to use the temporary coord interpolation opt-in test.
kueda Jan 31, 2019
d99f9e4
Merge branch 'master' into 2037-coordinate-obscuration-by-day
kueda Feb 26, 2019
678fca3
Update obscuration after changing interpolation pref; improve perform…
kueda Feb 28, 2019
b3eaebc
Fixed a bug obscuring coordinates.
kueda Feb 28, 2019
6124654
Fixed a spec.
kueda Feb 28, 2019
e991667
Separate context_user_geoprivacy and context_taxon_geoprivacy.
kueda Mar 1, 2019
06f2d21
Make obscuration test visible to non-staff.
kueda Mar 1, 2019
34b3124
Only show test to signed in users.
kueda Mar 1, 2019
b0d4fca
Merge branch 'master' into 2037-coordinate-obscuration-by-day
kueda Mar 15, 2019
c92d0d2
Moved opt-in test button to users/edit, show context obscured obs as …
kueda Mar 16, 2019
a94ce3c
Cleaned out some extranous db structure, fix dj unique hash issue.
kueda Mar 19, 2019
600c1d5
Merge branch 'master' into 2037-coordinate-obscuration-by-day
kueda Mar 20, 2019
469fdfc
more preloading for bulk obs indexing
pleary Mar 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/assets/javascripts/i18n/translations.js
Expand Up @@ -13306,6 +13306,10 @@ I18n.translations["en"] = {
"reviewed": "Reviewed",
"rg_observations": "RG Observations",
"running_total": "Running Total",
"same_day_obscured": "Another observation by this person on this day is obscured",
"same_day_obscured_desc": "When coordinates are obscured because of a threatened taxon, all other\nobservations by that person on that day are also obscured to prevent people\nfrom guessing the coordinates based on observations made around the same\ntime. Individuals can also apply this protection to coordinates they have\nchosen to obscure.\n",
"same_day_private": "Another observation by this person on this day is private",
"same_day_private_desc": "When coordinates are hidden because of a threatened taxon, all other\nobservations by that person on that day are also hidden to prevent people\nfrom guessing the coordinates based on observations made around the same\ntime. Individuals can also apply this protection to coordinates they have\nchosen to hide.\n",
"satellite": "satellite",
"save": "Save",
"save_photos": "Save photos",
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/users_controller.rb
Expand Up @@ -1095,6 +1095,8 @@ def whitelist_params
:prefers_common_names,
:prefers_scientific_name_first,
:prefers_no_place,
:prefers_coordinate_interpolation_protection,
:prefers_coordinate_interpolation_protection_test,
:search_place_id,
:site_id,
:test_groups,
Expand Down
10 changes: 8 additions & 2 deletions app/es_indices/observation_index.rb
Expand Up @@ -7,13 +7,13 @@ class Observation < ActiveRecord::Base
attr_accessor :indexed_place_ids, :indexed_private_place_ids, :indexed_private_places

scope :load_for_index, -> { includes(
{ user: :flags }, :confirmed_reviews, :flags,
{ user: [ :flags, :stored_preferences ] }, :confirmed_reviews, :flags,
:observation_links, :quality_metrics,
:votes_for, :stored_preferences, :tags,
{ annotations: :votes_for },
:photos,
{ sounds: :user },
{ identifications: :stored_preferences }, :project_observations,
{ identifications: [ :stored_preferences, :taxon ] }, :project_observations,
{ taxon: [ :taxon_names, :conservation_statuses ] },
{ observation_field_values: :observation_field },
{ comments: [ { user: :flags }, :flags ] } ) }
Expand Down Expand Up @@ -97,6 +97,9 @@ class Observation < ActiveRecord::Base
indexes :created_time_zone, type: "keyword", index: false
indexes :geoprivacy, type: "keyword"
indexes :taxon_geoprivacy, type: "keyword"
indexes :context_geoprivacy, type: "keyword"
indexes :context_user_geoprivacy, type: "keyword"
indexes :context_taxon_geoprivacy, type: "keyword"
indexes :observed_time_zone, type: "keyword", index: false
indexes :quality_grade, type: "keyword"
indexes :time_zone_offset, type: "keyword", index: false
Expand Down Expand Up @@ -161,6 +164,9 @@ def as_indexed_json(options={})
license_code: license ? license.downcase : nil,
geoprivacy: geoprivacy,
taxon_geoprivacy: taxon_geoprivacy,
context_geoprivacy: context_geoprivacy,
context_user_geoprivacy: context_user_geoprivacy,
context_taxon_geoprivacy: context_taxon_geoprivacy,
map_scale: map_scale,
oauth_application_id: application_id_to_index,
community_taxon_id: community_taxon_id,
Expand Down
165 changes: 119 additions & 46 deletions app/models/observation.rb
Expand Up @@ -71,6 +71,10 @@ class Observation < ActiveRecord::Base
attr_accessor :owners_identification_from_vision_requested
attr_accessor :localize_locale
attr_accessor :localize_place

# Track whether obscuration has changed over the life of this instance
attr_accessor :obscuration_changed
attr_accessor :skip_reassess_same_day_observations

def captive_flag
@captive_flag ||= !quality_metrics.detect{|qm|
Expand Down Expand Up @@ -357,16 +361,14 @@ def captive_flag=(v)
:set_taxon_from_taxon_name,
:keep_old_taxon_id,
:set_latlon_from_place_guess,
:reset_private_coordinates_if_coordinates_changed,
:normalize_geoprivacy,
:set_license,
:trim_user_agent,
:update_identifications,
:set_taxon_geoprivacy,
:set_community_taxon_before_save,
:set_taxon_from_probable_taxon,
:obscure_coordinates_for_geoprivacy,
:obscure_coordinates_for_threatened_taxa,
:reassess_coordinate_obscuration,
:set_geom_from_latlon,
:set_place_guess_from_latlon,
:obscure_place_guess,
Expand All @@ -386,7 +388,8 @@ def captive_flag=(v)
:set_captive,
:set_taxon_photo,
:create_observation_review,
:reassess_annotations
:reassess_annotations,
:reassess_same_day_observations
after_create :set_uri, :update_user_counter_caches_after_create
before_destroy :keep_old_taxon_id
after_destroy :refresh_lists_after_destroy, :refresh_check_lists,
Expand Down Expand Up @@ -1396,66 +1399,89 @@ def coordinates_viewable_by?(viewer)
end
false
end

def reset_private_coordinates_if_coordinates_changed
if (latitude_changed? || longitude_changed?)
self.private_latitude = nil
self.private_longitude = nil
end
true
end

def normalize_geoprivacy
self.geoprivacy = nil unless GEOPRIVACIES.include?(geoprivacy)
true
end

def obscure_coordinates_for_geoprivacy
self.geoprivacy = nil if geoprivacy.blank?
return true if geoprivacy.blank? && !geoprivacy_changed?
case geoprivacy
when PRIVATE
obscure_coordinates unless coordinates_obscured?
self.latitude, self.longitude = [nil, nil]
when OBSCURED
obscure_coordinates unless coordinates_obscured?

def reassess_coordinate_obscuration
geoprivacies = [geoprivacy, taxon_geoprivacy, context_geoprivacy]
if geoprivacies.include?( PRIVATE )
hide_coordinates
elsif geoprivacies.include?( OBSCURED )
obscure_coordinates
else
unobscure_coordinates
end
true
end

def obscure_coordinates_for_threatened_taxa
lat = private_latitude.blank? ? latitude : private_latitude
lon = private_longitude.blank? ? longitude : private_longitude
t = taxon || community_taxon
target_taxon_ids = [[t.try(:id)] + identifications.current.pluck(:taxon_id)].flatten.compact.uniq
taxon_geoprivacy = Taxon.max_geoprivacy( target_taxon_ids, latitude: lat, longitude: lon )
case taxon_geoprivacy
when OBSCURED
obscure_coordinates unless coordinates_obscured?
when PRIVATE
unless coordinates_private?
obscure_coordinates
self.latitude, self.longitude = [nil, nil]
end
else
unobscure_coordinates

# I.e. what should the geoprivacy be given other observations in context.
# Currently the "context" is observations made by the same user on the same
# day.
def context_geoprivacy
geoprivacies = [context_taxon_geoprivacy]
geoprivacies << context_user_geoprivacy if user.prefers_coordinate_interpolation_protection?
if geoprivacies.include?( PRIVATE )
return PRIVATE
end
if geoprivacies.include?( OBSCURED )
return OBSCURED
end
true
end

def context_user_geoprivacy
return if observed_on.blank?
return unless user && user.prefers_coordinate_interpolation_protection_test?
scope = user.observations.on( observed_on )
scope = scope.where( "id != ?", id ) if persisted?
geoprivacies = scope.pluck(:geoprivacy).flatten.uniq
if geoprivacies.include?( PRIVATE )
return PRIVATE
end
if geoprivacies.include?( OBSCURED )
return OBSCURED
end
end

def context_taxon_geoprivacy
return if observed_on.blank?
return unless user && user.prefers_coordinate_interpolation_protection_test?
scope = user.observations.on( observed_on )
scope = scope.where( "id != ?", id ) if persisted?
geoprivacies = scope.pluck(:taxon_geoprivacy).flatten.uniq
if geoprivacies.include?( PRIVATE )
return PRIVATE
end
if geoprivacies.include?( OBSCURED )
return OBSCURED
end
end

def hide_coordinates
return if coordinates_private?
obscure_coordinates
self.latitude, self.longitude = [nil, nil]
end

def obscure_coordinates
return if latitude.blank? || longitude.blank?
return if obscuration_changed
if latitude.blank? || longitude.blank?
self.obscuration_changed = geoprivacy_changed?
return
end
if latitude_changed? || longitude_changed?
self.private_latitude = latitude
self.private_longitude = longitude
else
self.private_latitude ||= latitude
self.private_longitude ||= longitude
end
self.latitude, self.longitude = Observation.random_neighbor_lat_lon( private_latitude, private_longitude )
set_geom_from_latlon
if private_latitude_changed? || private_longitude_changed?
self.latitude, self.longitude = Observation.random_neighbor_lat_lon( private_latitude, private_longitude )
set_geom_from_latlon
self.obscuration_changed = true
end
true
end

Expand Down Expand Up @@ -1507,6 +1533,7 @@ def unobscure_coordinates
self.private_latitude = nil
self.private_longitude = nil
set_geom_from_latlon
self.obscuration_changed = true
end

def iconic_taxon_name
Expand Down Expand Up @@ -1741,11 +1768,19 @@ def self.reassess_coordinates_for_observations_of( taxon, options = {} )
end
end

def self.reassess_coordinates_for_observations_by( user )
batch_size = 500
scope = Observation.by( user )
scope.find_in_batches( batch_size: batch_size ) do |batch|
reassess_coordinates_of( batch )
end
end

def self.reassess_coordinates_of( observations )
observations.each do |o|
o.obscure_coordinates_for_threatened_taxa
o.obscure_place_guess
o.set_taxon_geoprivacy
o.reassess_coordinate_obscuration
o.obscure_place_guess
next unless o.coordinates_changed? || o.place_guess_changed? || o.taxon_geoprivacy_changed?
Observation.where( id: o.id ).update_all(
latitude: o.latitude,
Expand Down Expand Up @@ -2639,6 +2674,44 @@ def reassess_annotations
true
end

def reassess_same_day_observations
return true if skip_reassess_same_day_observations
return true unless obscuration_changed || observed_on_changed?
unless observed_on.blank?
Observation.
delay(
priority: USER_INTEGRITY_PRIORITY,
unique_hash: {
"Observation::reassess_obscuration_by_user_and_date": [user_id, observed_on.to_s]
}
).
reassess_obscuration_by_user_and_date( user_id, observed_on.to_s )
end
if !observed_on_was.blank? && observed_on_changed?
Observation.
delay(
priority: USER_INTEGRITY_PRIORITY,
unique_hash: {
"Observation::reassess_obscuration_by_user_and_date": [user_id, observed_on_was.to_s]
}
).
reassess_obscuration_by_user_and_date( user_id, observed_on_was.to_s )
end
true
end

def self.reassess_obscuration_by_user_and_date( user, date )
user = user.is_a?( User ) ? user : User.find_by_id( user )
return unless user
date = date.is_a?( Date ) ? date : Date.parse( date )
return unless date
user.observations.on( date ).each do |o|
o.reassess_coordinate_obscuration
o.skip_reassess_same_day_observations = true
o.save! if o.changed?
end
end

def create_deleted_observation
DeletedObservation.create(
:observation_id => id,
Expand Down
1 change: 1 addition & 0 deletions app/models/taxon.rb
Expand Up @@ -1470,6 +1470,7 @@ def threatened_in_lat_lon?(lat, lon)
end

def self.max_geoprivacy( taxon_ids, options = {} )
return if taxon_ids.blank?
target_taxon_ids = [
taxon_ids,
Taxon.where( "id IN (?)", taxon_ids).pluck(:ancestry).map{|a| a.to_s.split( "/" ).map(&:to_i)}
Expand Down
12 changes: 11 additions & 1 deletion app/models/user.rb
Expand Up @@ -83,6 +83,8 @@ class User < ActiveRecord::Base
preference :scientific_name_first, :boolean, default: false
preference :no_place, :boolean, default: false
preference :medialess_obs_maps, :boolean, default: false
preference :coordinate_interpolation_protection, default: false
preference :coordinate_interpolation_protection_test, default: false
preference :forum_topics_on_dashboard, :boolean, default: true

NOTIFICATION_PREFERENCES = %w(
Expand Down Expand Up @@ -227,6 +229,7 @@ def followers
after_save :revoke_access_tokens_by_suspended_user
after_save :restore_access_tokens_by_suspended_user
after_update :set_community_taxa_if_pref_changed
after_update :reassess_coordinate_obscuration_if_pref_changed
after_update :update_photo_properties
after_update :update_life_list
after_create :create_default_life_list
Expand Down Expand Up @@ -1042,12 +1045,19 @@ def restore_access_tokens_by_suspended_user
end

def set_community_taxa_if_pref_changed
if prefers_community_taxa_changed? && ! id.blank?
if prefers_community_taxa_changed? && !id.blank?
Observation.delay(:priority => USER_INTEGRITY_PRIORITY).set_community_taxa(:user => id)
end
true
end

def reassess_coordinate_obscuration_if_pref_changed
if prefers_coordinate_interpolation_protection_changed? && !id.blank?
Observation.delay( priority: USER_INTEGRITY_PRIORITY ).reassess_coordinates_for_observations_by( id )
end
true
end

def update_life_list
if login_changed? && life_list
life_list.update_attributes( title: life_list.title.gsub( /#{login_was}/, login ) )
Expand Down
1 change: 1 addition & 0 deletions app/views/observations/show.html.haml
Expand Up @@ -65,6 +65,7 @@
prefers_medialess_obs_maps: current_user.prefers_medialess_obs_maps,
preferred_observation_license: (current_user.preferred_observation_license || "").downcase,
prefers_scientific_name_first: current_user.prefers_scientific_name_first,
prefers_coordinate_interpolation_protection_test: current_user.prefers_coordinate_interpolation_protection_test,
blockedUserHashes: current_user.user_blocks.map{|ub| Digest::MD5.hexdigest( ub.blocked_user_id.to_s ) },
blockedByUserHashes: current_user.user_blocks_as_blocked_user.map{|ub| Digest::MD5.hexdigest( ub.user_id.to_s ) },
trusted_user_ids: current_user.friendships.where( "trust" ).pluck(:friend_id),
Expand Down
17 changes: 17 additions & 0 deletions app/views/shared/_footer.html.haml
@@ -1,4 +1,21 @@
- site = @site || CONFIG
- if is_admin? && controller.controller_name == "users" && action_name == "edit"
.bootstrap
.container
.row
.col-xs-12
.box.text-center.upstacked.alert.alert-warning
- if current_user.prefers_coordinate_interpolation_protection_test?
= form_for current_user do |f|
= f.hidden_field :prefers_coordinate_interpolation_protection_test, value: false
= f.submit "Opt-out of coordinate obscuration by day", class: "btn btn-warning"
- else
= form_for current_user do |f|
= f.hidden_field :prefers_coordinate_interpolation_protection_test, value: true
= f.submit "Opt-in to coordinate obscuration by day", class: "btn btn-success"
%p
This will automatically obscure all observations you make on the same day as an observation of a threatened taxon. During this test, it will only apply to observations you create or update after opting in to the test, so to see the change on past observations, you will have to change their coordinates or add an ID of a threatened taxon.

#footer.bootstrap
.container
.row
Expand Down
8 changes: 8 additions & 0 deletions app/views/users/edit.html.erb
Expand Up @@ -155,6 +155,14 @@
[t(:curators), User::PREFERRED_OBSERVATION_FIELDS_BY_CURATORS],
[t(:only_you), User::PREFERRED_OBSERVATION_FIELDS_BY_OBSERVER]
], :description => t(:observation_fields_by_preferences_description) %>

<% if current_user.prefers_coordinate_interpolation_protection_test? %>
<h3><%=t :privacy %></h3>
<%= f.check_box "prefers_coordinate_interpolation_protection",
label: t( "views.users.edit.same_day_geoprivacy_protection" ),
label_after: true,
description: t( "views.users.edit.prefers_coordinate_interpolation_protection_desc" ) %>
<% end %>
</div>

<div class="column span-18">
Expand Down