<commit_before>#encoding: utf-8
class Observation < ActiveRecord::Base
  has_subscribers :to => {
    :comments => {:notification => "activity", :include_owner => true},
    :identifications => {:notification => "activity", :include_owner => true}
  }
  notifies_subscribers_of :user, :notification => "created_observations"
  notifies_subscribers_of :public_places, :notification => "new_observations", 
    :queue_if => lambda {|observation|
      observation.georeferenced? && !observation.taxon_id.blank?
    },
    :if => lambda {|observation, place, subscription|
      return false unless observation.georeferenced?
      return true if subscription.taxon_id.blank?
      return false if observation.taxon.blank?
      observation.taxon.ancestor_ids.include?(subscription.taxon_id)
    }
  notifies_subscribers_of :taxon_and_ancestors, :notification => "new_observations", 
    :queue_if => lambda {|observation| !observation.taxon_id.blank? },
    :if => lambda {|observation, taxon, subscription|
      return true if observation.taxon_id == taxon.id
      return false if observation.taxon.blank?
      observation.taxon.ancestor_ids.include?(subscription.resource_id)
    }
  acts_as_taggable
  acts_as_flaggable
  
  include Ambidextrous
  
  # Set to true if you want to skip the expensive updating of all the user's
  # lists after saving.  Useful if you're saving many observations at once and
  # you want to update lists in a batch
  attr_accessor :skip_refresh_lists, :skip_identifications
  
  # Set if you need to set the taxon from a name separate from the species 
  # guess
  attr_accessor :taxon_name
  
  # licensing extras
  attr_accessor :make_license_default
  attr_accessor :make_licenses_same
  
  MASS_ASSIGNABLE_ATTRIBUTES = [:make_license_default, :make_licenses_same]
  
  M_TO_OBSCURE_THREATENED_TAXA = 10000
  OUT_OF_RANGE_BUFFER = 5000 # meters
  PLANETARY_RADIUS = 6370997.0
  DEGREES_PER_RADIAN = 57.2958
  FLOAT_REGEX = /[-+]?[0-9]*\.?[0-9]+/
  COORDINATE_REGEX = /[^\d\,]*?(#{FLOAT_REGEX})[^\d\,]*?/
  LAT_LON_SEPARATOR_REGEX = /[\,\s]\s*/
  LAT_LON_REGEX = /#{COORDINATE_REGEX}#{LAT_LON_SEPARATOR_REGEX}#{COORDINATE_REGEX}/
  
  PRIVATE = "private"
  OBSCURED = "obscured"
  GEOPRIVACIES = [OBSCURED, PRIVATE]
  GEOPRIVACY_DESCRIPTIONS = {
    nil => "Everyone can see the coordinates unless the taxon is threatened.",
    OBSCURED => "Public coordinates shown as a random point within " + 
      "#{M_TO_OBSCURE_THREATENED_TAXA / 1000}KM of the true coordinates. " +
      "True coordinates are only visible to you and the curators of projects " + 
      "to which you add the observation.",
    PRIVATE => "Coordinates completely hidden from public maps, true " + 
      "coordinates only visible to you and the curators of projects to " + 
      "which you add the observation. Observations with private " + 
      "coordinates will still be used to verify place check lists.",
  }
  CASUAL_GRADE = "casual"
  RESEARCH_GRADE = "research"
  QUALITY_GRADES = [CASUAL_GRADE, RESEARCH_GRADE]
  
  LICENSES = [
    ["CC-BY", "Attribution", "This license lets others distribute, remix, tweak, and build upon your work, even commercially, as long as they credit you for the original creation. This is the most accommodating of licenses offered. Recommended for maximum dissemination and use of licensed materials."],
    ["CC-BY-NC", "Attribution-NonCommercial", "This license lets others remix, tweak, and build upon your work non-commercially, and although their new works must also acknowledge you and be non-commercial, they don't have to license their derivative works on the same terms."],
    ["CC-BY-SA", "Attribution-ShareAlike", "This license lets others remix, tweak, and build upon your work even for commercial purposes, as long as they credit you and license their new creations under the identical terms. All new works based on yours will carry the same license, so any derivatives will also allow commercial use."],
    ["CC-BY-ND", "Attribution-NoDerivs", "This license allows for redistribution, commercial and non-commercial, as long as it is passed along unchanged and in whole, with credit to you."],
    ["CC-BY-NC-SA", "Attribution-NonCommercial-ShareAlike", "This license lets others remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms."],
    ["CC-BY-NC-ND", "Attribution-NonCommercial-NoDerivs", "This license is the most restrictive of the six main licenses, only allowing others to download your works and share them with others as long as they credit you, but they can't change them in any way or use them commercially."]
  ]
  LICENSE_CODES = LICENSES.map{|row| row.first}
  LICENSES.each do |code, name, description|
    const_set code.gsub(/\-/, '_'), code
  end
  PREFERRED_LICENSES = [CC_BY, CC_BY_NC]
  CSV_COLUMNS = [
    "id", 
    "species_guess",
    "scientific_name", 
    "common_name", 
    "iconic_taxon_name",
    "taxon_id",
    "id_please",
    "num_identification_agreements",
    "num_identification_disagreements",
    "observed_on_string",
    "observed_on", 
    "time_observed_at",
    "time_zone",
    "place_guess",
    "latitude", 
    "longitude",
    "positional_accuracy",
    "private_latitude",
    "private_longitude",
    "private_positional_accuracy",
    "geoprivacy",
    "positioning_method",
    "positioning_device",
    "out_of_range",
    "user_id", 
    "user_login",
    "created_at",
    "updated_at",
    "quality_grade",
    "license",
    "url", 
    "image_url", 
    "tag_list",
    "description",
  ]
  
  belongs_to :user, :counter_cache => true
  belongs_to :taxon, :counter_cache => true
  belongs_to :iconic_taxon, :class_name => 'Taxon', 
                            :foreign_key => 'iconic_taxon_id'
  has_many :observation_photos, :dependent => :destroy, :order => "id asc"
  has_many :photos, :through => :observation_photos
  
  # note last_observation and first_observation on listed taxa will get reset 
  # by CheckList.refresh_with_observation
  has_many :listed_taxa, :foreign_key => 'last_observation_id'
  has_many :first_listed_taxa, :class_name => "ListedTaxon", :foreign_key => 'first_observation_id'
  has_many :first_check_listed_taxa, :class_name => "ListedTaxon", :foreign_key => 'first_observation_id', :conditions => "listed_taxa.place_id IS NOT NULL"
  
  has_many :goal_contributions,
           :as => :contribution,
           :dependent => :destroy
  has_many :comments, :as => :parent, :dependent => :destroy
  has_many :identifications, :dependent => :destroy
  has_many :project_observations, :dependent => :destroy
  has_many :project_invitations, :dependent => :destroy
  has_many :projects, :through => :project_observations
  has_many :quality_metrics, :dependent => :destroy
  has_many :observation_field_values, :dependent => :destroy, :order => "id asc"
  has_many :observation_fields, :through => :observation_field_values
  has_many :observation_links
  has_and_belongs_to_many :posts
  
  define_index do
    indexes taxon.taxon_names.name, :as => :names
    indexes tags.name, :as => :tags
    indexes :species_guess, :sortable => true, :as => :species_guess
    indexes :description, :as => :description
    indexes :place_guess, :as => :place, :sortable => true
    indexes user.login, :as => :user, :sortable => true
    indexes :observed_on_string, :as => :observed_on_string
    has :user_id
    has :taxon_id
    
    # Sadly, the following doesn't work, because self_and_ancestors is not an
    # association.  I'm not entirely sure if there's a way to work the ancestry
    # query in as col in a SQL query on observations.  If at some point we
    # need to have the ancestor ids in the Sphinx index, though, we can always
    # add a col to the taxa table holding the ancestor IDs.  Kind of a
    # redundant, and it would slow down moves, but it might be worth it for
    # the snappy searches. --KMU 2009-04-4
    # has taxon.self_and_ancestors(:id), :as => :taxon_self_and_ancestors_ids
    
    has :photos_count, :as => :has_photos
    has :created_at, :sortable => true
    has :observed_on, :sortable => true
    has :iconic_taxon_id
    has :id_please, :as => :has_id_please
    has "latitude IS NOT NULL AND longitude IS NOT NULL", 
      :as => :has_geo, :type => :boolean
    has 'RADIANS(latitude)', :as => :latitude,  :type => :float
    has 'RADIANS(longitude)', :as => :longitude,  :type => :float
    
    # HACK: TS doesn't seem to include attributes in the GROUP BY correctly
    # for Postgres when using custom SQL attr definitions.  It may or may not 
    # be fixed in more up-to-date versions, but the issue has been raised: 
    # http://groups.google.com/group/thinking-sphinx/browse_thread/thread/e8397477b201d1e4
    has :latitude, :as => :fake_latitude
    has :longitude, :as => :fake_longitude<commit_msg>Sphinx doesn't like things without types, apparently.<commit_after>#encoding: utf-8
class Observation < ActiveRecord::Base
  has_subscribers :to => {
    :comments => {:notification => "activity", :include_owner => true},
    :identifications => {:notification => "activity", :include_owner => true}
  }
  notifies_subscribers_of :user, :notification => "created_observations"
  notifies_subscribers_of :public_places, :notification => "new_observations", 
    :queue_if => lambda {|observation|
      observation.georeferenced? && !observation.taxon_id.blank?
    },
    :if => lambda {|observation, place, subscription|
      return false unless observation.georeferenced?
      return true if subscription.taxon_id.blank?
      return false if observation.taxon.blank?
      observation.taxon.ancestor_ids.include?(subscription.taxon_id)
    }
  notifies_subscribers_of :taxon_and_ancestors, :notification => "new_observations", 
    :queue_if => lambda {|observation| !observation.taxon_id.blank? },
    :if => lambda {|observation, taxon, subscription|
      return true if observation.taxon_id == taxon.id
      return false if observation.taxon.blank?
      observation.taxon.ancestor_ids.include?(subscription.resource_id)
    }
  acts_as_taggable
  acts_as_flaggable
  
  include Ambidextrous
  
  # Set to true if you want to skip the expensive updating of all the user's
  # lists after saving.  Useful if you're saving many observations at once and
  # you want to update lists in a batch
  attr_accessor :skip_refresh_lists, :skip_identifications
  
  # Set if you need to set the taxon from a name separate from the species 
  # guess
  attr_accessor :taxon_name
  
  # licensing extras
  attr_accessor :make_license_default
  attr_accessor :make_licenses_same
  
  MASS_ASSIGNABLE_ATTRIBUTES = [:make_license_default, :make_licenses_same]
  
  M_TO_OBSCURE_THREATENED_TAXA = 10000
  OUT_OF_RANGE_BUFFER = 5000 # meters
  PLANETARY_RADIUS = 6370997.0
  DEGREES_PER_RADIAN = 57.2958
  FLOAT_REGEX = /[-+]?[0-9]*\.?[0-9]+/
  COORDINATE_REGEX = /[^\d\,]*?(#{FLOAT_REGEX})[^\d\,]*?/
  LAT_LON_SEPARATOR_REGEX = /[\,\s]\s*/
  LAT_LON_REGEX = /#{COORDINATE_REGEX}#{LAT_LON_SEPARATOR_REGEX}#{COORDINATE_REGEX}/
  
  PRIVATE = "private"
  OBSCURED = "obscured"
  GEOPRIVACIES = [OBSCURED, PRIVATE]
  GEOPRIVACY_DESCRIPTIONS = {
    nil => "Everyone can see the coordinates unless the taxon is threatened.",
    OBSCURED => "Public coordinates shown as a random point within " + 
      "#{M_TO_OBSCURE_THREATENED_TAXA / 1000}KM of the true coordinates. " +
      "True coordinates are only visible to you and the curators of projects " + 
      "to which you add the observation.",
    PRIVATE => "Coordinates completely hidden from public maps, true " + 
      "coordinates only visible to you and the curators of projects to " + 
      "which you add the observation. Observations with private " + 
      "coordinates will still be used to verify place check lists.",
  }
  CASUAL_GRADE = "casual"
  RESEARCH_GRADE = "research"
  QUALITY_GRADES = [CASUAL_GRADE, RESEARCH_GRADE]
  
  LICENSES = [
    ["CC-BY", "Attribution", "This license lets others distribute, remix, tweak, and build upon your work, even commercially, as long as they credit you for the original creation. This is the most accommodating of licenses offered. Recommended for maximum dissemination and use of licensed materials."],
    ["CC-BY-NC", "Attribution-NonCommercial", "This license lets others remix, tweak, and build upon your work non-commercially, and although their new works must also acknowledge you and be non-commercial, they don't have to license their derivative works on the same terms."],
    ["CC-BY-SA", "Attribution-ShareAlike", "This license lets others remix, tweak, and build upon your work even for commercial purposes, as long as they credit you and license their new creations under the identical terms. All new works based on yours will carry the same license, so any derivatives will also allow commercial use."],
    ["CC-BY-ND", "Attribution-NoDerivs", "This license allows for redistribution, commercial and non-commercial, as long as it is passed along unchanged and in whole, with credit to you."],
    ["CC-BY-NC-SA", "Attribution-NonCommercial-ShareAlike", "This license lets others remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms."],
    ["CC-BY-NC-ND", "Attribution-NonCommercial-NoDerivs", "This license is the most restrictive of the six main licenses, only allowing others to download your works and share them with others as long as they credit you, but they can't change them in any way or use them commercially."]
  ]
  LICENSE_CODES = LICENSES.map{|row| row.first}
  LICENSES.each do |code, name, description|
    const_set code.gsub(/\-/, '_'), code
  end
  PREFERRED_LICENSES = [CC_BY, CC_BY_NC]
  CSV_COLUMNS = [
    "id", 
    "species_guess",
    "scientific_name", 
    "common_name", 
    "iconic_taxon_name",
    "taxon_id",
    "id_please",
    "num_identification_agreements",
    "num_identification_disagreements",
    "observed_on_string",
    "observed_on", 
    "time_observed_at",
    "time_zone",
    "place_guess",
    "latitude", 
    "longitude",
    "positional_accuracy",
    "private_latitude",
    "private_longitude",
    "private_positional_accuracy",
    "geoprivacy",
    "positioning_method",
    "positioning_device",
    "out_of_range",
    "user_id", 
    "user_login",
    "created_at",
    "updated_at",
    "quality_grade",
    "license",
    "url", 
    "image_url", 
    "tag_list",
    "description",
  ]
  
  belongs_to :user, :counter_cache => true
  belongs_to :taxon, :counter_cache => true
  belongs_to :iconic_taxon, :class_name => 'Taxon', 
                            :foreign_key => 'iconic_taxon_id'
  has_many :observation_photos, :dependent => :destroy, :order => "id asc"
  has_many :photos, :through => :observation_photos
  
  # note last_observation and first_observation on listed taxa will get reset 
  # by CheckList.refresh_with_observation
  has_many :listed_taxa, :foreign_key => 'last_observation_id'
  has_many :first_listed_taxa, :class_name => "ListedTaxon", :foreign_key => 'first_observation_id'
  has_many :first_check_listed_taxa, :class_name => "ListedTaxon", :foreign_key => 'first_observation_id', :conditions => "listed_taxa.place_id IS NOT NULL"
  
  has_many :goal_contributions,
           :as => :contribution,
           :dependent => :destroy
  has_many :comments, :as => :parent, :dependent => :destroy
  has_many :identifications, :dependent => :destroy
  has_many :project_observations, :dependent => :destroy
  has_many :project_invitations, :dependent => :destroy
  has_many :projects, :through => :project_observations
  has_many :quality_metrics, :dependent => :destroy
  has_many :observation_field_values, :dependent => :destroy, :order => "id asc"
  has_many :observation_fields, :through => :observation_field_values
  has_many :observation_links
  has_and_belongs_to_many :posts
  
  define_index do
    indexes taxon.taxon_names.name, :as => :names
    indexes tags.name, :as => :tags
    indexes :species_guess, :sortable => true, :as => :species_guess
    indexes :description, :as => :description
    indexes :place_guess, :as => :place, :sortable => true
    indexes user.login, :as => :user, :sortable => true
    indexes :observed_on_string, :as => :observed_on_string
    has :user_id
    has :taxon_id
    
    # Sadly, the following doesn't work, because self_and_ancestors is not an
    # association.  I'm not entirely sure if there's a way to work the ancestry
    # query in as col in a SQL query on observations.  If at some point we
    # need to have the ancestor ids in the Sphinx index, though, we can always
    # add a col to the taxa table holding the ancestor IDs.  Kind of a
    # redundant, and it would slow down moves, but it might be worth it for
    # the snappy searches. --KMU 2009-04-4
    # has taxon.self_and_ancestors(:id), :as => :taxon_self_and_ancestors_ids
    
    has :photos_count, :as => :has_photos, :type => :integer
    has :created_at, :sortable => true
    has :observed_on, :sortable => true
    has :iconic_taxon_id
    has :id_please, :as => :has_id_please
    has "latitude IS NOT NULL AND longitude IS NOT NULL", 
      :as => :has_geo, :type => :boolean
    has 'RADIANS(latitude)', :as => :latitude,  :type => :float
    has 'RADIANS(longitude)', :as => :longitude,  :type => :float
    
    # HACK: TS doesn't seem to include attributes in the GROUP BY correctly
    # for Postgres when using custom SQL attr definitions.  It may or may not 
    # be fixed in more up-to-date versions, but the issue has been raised: 
    # http://groups.google.com/group/thinking-sphinx/browse_thread/thread/e8397477b201d1e4
    has :latitude, :as => :fake_latitude
    has :longitude, :as => :fake_longitude
{'fraction_non_alphanumeric': 0.10127149498683695, 'fraction_numerical': 0.005713325491514031, 'mean_word_length': 3.9525658807212207, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 134, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7215843', 'n_tokens_mistral': 5954, 'n_tokens_neox': 5666, 'n_words': 1721}