<commit_before>class User < ActiveRecord::Base
  include ActsAsSpammable::User

  acts_as_voter
  acts_as_spammable :fields => [ :description ],
                    :comment_type => "item-description"

  # If the user has this role, has_role? will always return true
  JEDI_MASTER_ROLE = 'admin'
  
  devise :database_authenticatable, :registerable, :suspendable,
         :recoverable, :rememberable, :confirmable, :validatable, 
         :encryptable, :encryptor => :restful_authentication_sha1
  handle_asynchronously :send_devise_notification
  
  # set user.skip_email_validation = true if you want to, um, skip email validation before creating+saving
  attr_accessor :skip_email_validation
  attr_accessor :skip_registration_email
  
  # licensing extras
  attr_accessor   :make_observation_licenses_same
  attr_accessor   :make_photo_licenses_same
  attr_accessor   :make_sound_licenses_same

  attr_accessor :html

  preference :project_journal_post_email_notification, :boolean, :default => true
  preference :comment_email_notification, :boolean, :default => true
  preference :identification_email_notification, :boolean, :default => true
  preference :message_email_notification, :boolean, :default => true
  preference :no_email, :boolean, :default => false
  preference :project_invitation_email_notification, :boolean, :default => true
  preference :mention_email_notification, :boolean, :default => true
  preference :lists_by_login_sort, :string, :default => "id"
  preference :lists_by_login_order, :string, :default => "asc"
  preference :per_page, :integer, :default => 30
  preference :gbif_sharing, :boolean, :default => true
  preference :observation_license, :string
  preference :photo_license, :string
  preference :sound_license, :string
  preference :share_observations_on_facebook, :boolean, :default => true
  preference :share_observations_on_twitter, :boolean, :default => true
  preference :automatic_taxonomic_changes, :boolean, :default => true
  preference :receive_mentions, :boolean, :default => true
  preference :observations_view, :string
  preference :observations_search_subview, :string
  preference :observations_search_map_type, :string
  preference :community_taxa, :boolean, :default => true
  PREFERRED_OBSERVATION_FIELDS_BY_ANYONE = "anyone"
  PREFERRED_OBSERVATION_FIELDS_BY_CURATORS = "curators"
  PREFERRED_OBSERVATION_FIELDS_BY_OBSERVER = "observer"
  preference :observation_fields_by, :string, :default => PREFERRED_OBSERVATION_FIELDS_BY_ANYONE
  PROJECT_ADDITION_BY_ANY = "any"
  PROJECT_ADDITION_BY_JOINED = "joined"
  PROJECT_ADDITION_BY_NONE = "none"
  preference :project_addition_by, :string, default: PROJECT_ADDITION_BY_ANY
  preference :location_details, :boolean, default: false
  preference :redundant_identification_notifications, :boolean, default: true
  preference :skip_coarer_id_modal, default: false
  preference :hide_observe_onboarding, default: false
  preference :hide_follow_onboarding, default: false
  preference :hide_activity_onboarding, default: false
  preference :hide_getting_started_onboarding, default: false
  preference :hide_updates_by_you_onboarding, default: false
  preference :hide_comments_onboarding, default: false
  
  SHARING_PREFERENCES = %w(share_observations_on_facebook share_observations_on_twitter)
  NOTIFICATION_PREFERENCES = %w(comment_email_notification identification_email_notification 
    mention_email_notification message_email_notification
    project_invitation_email_notification project_journal_post_email_notification)
  
  belongs_to :life_list, :dependent => :destroy
  has_many  :provider_authorizations, :dependent => :delete_all
  has_one  :flickr_identity, :dependent => :delete
  # has_one  :picasa_identity, :dependent => :delete
  has_one  :soundcloud_identity, :dependent => :delete
  has_many :observations, :dependent => :destroy
  has_many :deleted_observations
  has_many :deleted_photos
  
  # Some interesting ways to map self-referential relationships in rails
  has_many :friendships, :dependent => :destroy
  has_many :friends, :through => :friendships
  has_many :stalkerships, :class_name => 'Friendship', :foreign_key => 'friend_id', :dependent => :destroy
  has_many :followers, :through => :stalkerships,  :source => 'user'
  
  has_many :lists, :dependent => :destroy
  has_many :life_lists
  has_many :identifications, :dependent => :destroy
  has_many :identifications_for_others,
    -> { where("identifications.user_id != observations.user_id AND identifications.current = true").
         joins(:observation) }, :class_name => "Identification"
  has_many :photos, :dependent => :destroy
  has_many :posts #, :dependent => :destroy
  has_many :journal_posts, :class_name => "Post", :as => :parent, :dependent => :destroy
  has_many :trips, -> { where("posts.type = 'Trip'") }, :class_name => "Post", :foreign_key => "user_id"
  has_many :taxon_links, :dependent => :nullify
  has_many :comments, :dependent => :destroy
  has_many :projects
  has_many :project_users, :dependent => :destroy
  has_many :project_user_invitations, :dependent => :nullify
  has_many :project_user_invitations_received, :dependent => :delete_all, :class_name => "ProjectUserInvitation"
  has_many :listed_taxa, :dependent => :nullify
  has_many :invites, :dependent => :nullify
  has_many :quality_metrics, :dependent => :destroy
  has_many :sources, :dependent => :nullify
  has_many :places, :dependent => :nullify
  has_many :messages, :dependent => :destroy
  has_many :delivered_messages, -> { where("messages.from_user_id != messages.user_id") }, :class_name => "Message", :foreign_key => "from_user_id"
  has_many :guides, :dependent => :destroy, :inverse_of => :user
  has_many :observation_fields, :dependent => :nullify, :inverse_of => :user
  has_many :observation_field_values, :dependent => :nullify, :inverse_of => :user
  has_many :updated_observation_field_values, :dependent => :nullify, :inverse_of => :updater, :foreign_key => "updater_id", :class_name => "ObservationFieldValue"
  has_many :guide_users, :inverse_of => :user, :dependent => :delete_all
  has_many :editing_guides, :through => :guide_users, :source => :guide
  has_many :created_guide_sections, :class_name => "GuideSection", :foreign_key => "creator_id", :inverse_of => :creator, :dependent => :nullify
  has_many :updated_guide_sections, :class_name => "GuideSection", :foreign_key => "updater_id", :inverse_of => :updater, :dependent => :nullify
  
  has_attached_file :icon,
    :styles => { :original => "2048x2048>", :medium => "300x300>", :thumb => "48x48#", :mini => "16x16#" },
    :processors => [:deanimator],
    :path => ":rails_root/public/attachments/:class/:attachment/:id-:style.:icon_type_extension",
    :url => "#{ CONFIG.attachments_host }/attachments/:class/:attachment/:id-:style.:icon_type_extension",
    :default_url => "/attachment_defaults/:class/:attachment/defaults/:style.png"

  # Roles
  has_and_belongs_to_many :roles, -> { uniq }
  
  has_subscribers
  has_many :subscriptions, :dependent => :delete_all
  has_many :updates, :foreign_key => :subscriber_id, :dependent => :delete_all
  has_many :flow_tasks
  has_many :project_observations, dependent: :nullify 
  belongs_to :site, :inverse_of => :users
  belongs_to :place, :inverse_of => :users

  before_validation :download_remote_icon, :if => :icon_url_provided?
  before_validation :strip_name, :strip_login
  before_save :set_time_zone
  before_save :whitelist_licenses
  before_save :get_lat_lon_from_ip_if_last_ip_changed
  before_create :set_locale
  after_save :update_observation_licenses
  after_save :update_photo_licenses
  after_save :update_sound_licenses
  after_save :update_observation_sites_later
  after_save :destroy_messages_by_suspended_user
  after_update :set_community_taxa_if_pref_changed
  after_create :create_default_life_list
  after_create :set_uri
  after_destroy :create_deleted_user
  
  validates_presence_of :icon_url, :if => :icon_url_provided?, :message => 'is invalid or inaccessible'
  validates_attachment_content_type :icon, :content_type => [/jpe?g/i, /png/i, /gif/i], 
    :message => "must be JPG, PNG, or GIF"

  validates_presence_of     :login
  
  MIN_LOGIN_SIZE = 3
  MAX_LOGIN_SIZE = 40
  
  # Regexes from restful_authentication
  LOGIN_PATTERN     = "[A-z][\\\w\\\-_]+"
  login_regex       = /\A#{ LOGIN_PATTERN }\z/                          # ASCII, strict
  bad_login_message = "use only letters, numbers, and -_ please.".freeze
  email_name_regex  = '[\w\.%\+\-]+'.freeze
  domain_head_regex = '(?:[A-Z0-9\-]+\.)+'.freeze
  domain_tld_regex  = '(?:[A-Z]+)'.freeze
  email_regex       = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i
  bad_email_message = "should look like an email address.".freeze
  
  validates_length_of       :login,     within: MIN_LOGIN_SIZE..MAX_LOGIN_SIZE
  validates_uniqueness_of   :login
  validates_format_of       :login,     with: login_regex, message: bad_login_message
  validates_exclusion_of    :login,     in: %w(password new edit create update delete destroy)

  validates_exclusion_of    :password,     in: %w(password)

  validates_length_of       :name,      maximum: 100, allow_blank: true

  validates_format_of       :email,     with: email_regex, message: bad_email_message, allow_blank: true
  validates_length_of       :email,     within: 6..100, allow_blank: true
  validates_length_of       :time_zone, minimum: 4, allow_nil: true
  
  scope :order_by, Proc.new { |sort_by, sort_dir|
    sort_dir ||= 'DESC'
    order("? ?", sort_by, sort_dir)
  }
  scope :curators, -> { joins(:roles).where("roles.name IN ('curator', 'admin')") }
  scope :admins, -> { joins(:roles).where("roles.name = 'admin'") }
  scope :active, -> { where("suspended_at IS NULL") }

  # only validate_presence_of email if user hasn't auth'd via a 3rd-party provider
  # you can also force skipping email validation by setting u.skip_email_validation=true before you save
  # (this option is necessary because the User is created before the associated ProviderAuthorization)
  # This is not a normal validation b/c email validation happens in Devise, which looks for this method
  def email_required?
    !(skip_email_validation || provider_authorizations.count > 0)
  end
  
  def icon_url_provided?
    !self.icon.present? && !self.icon_url.blank?
  end

  def user_icon_url
    return nil if icon.blank?<commit_msg>changed time_zone char validation length to 3<commit_after>class User < ActiveRecord::Base
  include ActsAsSpammable::User

  acts_as_voter
  acts_as_spammable :fields => [ :description ],
                    :comment_type => "item-description"

  # If the user has this role, has_role? will always return true
  JEDI_MASTER_ROLE = 'admin'
  
  devise :database_authenticatable, :registerable, :suspendable,
         :recoverable, :rememberable, :confirmable, :validatable, 
         :encryptable, :encryptor => :restful_authentication_sha1
  handle_asynchronously :send_devise_notification
  
  # set user.skip_email_validation = true if you want to, um, skip email validation before creating+saving
  attr_accessor :skip_email_validation
  attr_accessor :skip_registration_email
  
  # licensing extras
  attr_accessor   :make_observation_licenses_same
  attr_accessor   :make_photo_licenses_same
  attr_accessor   :make_sound_licenses_same

  attr_accessor :html

  preference :project_journal_post_email_notification, :boolean, :default => true
  preference :comment_email_notification, :boolean, :default => true
  preference :identification_email_notification, :boolean, :default => true
  preference :message_email_notification, :boolean, :default => true
  preference :no_email, :boolean, :default => false
  preference :project_invitation_email_notification, :boolean, :default => true
  preference :mention_email_notification, :boolean, :default => true
  preference :lists_by_login_sort, :string, :default => "id"
  preference :lists_by_login_order, :string, :default => "asc"
  preference :per_page, :integer, :default => 30
  preference :gbif_sharing, :boolean, :default => true
  preference :observation_license, :string
  preference :photo_license, :string
  preference :sound_license, :string
  preference :share_observations_on_facebook, :boolean, :default => true
  preference :share_observations_on_twitter, :boolean, :default => true
  preference :automatic_taxonomic_changes, :boolean, :default => true
  preference :receive_mentions, :boolean, :default => true
  preference :observations_view, :string
  preference :observations_search_subview, :string
  preference :observations_search_map_type, :string
  preference :community_taxa, :boolean, :default => true
  PREFERRED_OBSERVATION_FIELDS_BY_ANYONE = "anyone"
  PREFERRED_OBSERVATION_FIELDS_BY_CURATORS = "curators"
  PREFERRED_OBSERVATION_FIELDS_BY_OBSERVER = "observer"
  preference :observation_fields_by, :string, :default => PREFERRED_OBSERVATION_FIELDS_BY_ANYONE
  PROJECT_ADDITION_BY_ANY = "any"
  PROJECT_ADDITION_BY_JOINED = "joined"
  PROJECT_ADDITION_BY_NONE = "none"
  preference :project_addition_by, :string, default: PROJECT_ADDITION_BY_ANY
  preference :location_details, :boolean, default: false
  preference :redundant_identification_notifications, :boolean, default: true
  preference :skip_coarer_id_modal, default: false
  preference :hide_observe_onboarding, default: false
  preference :hide_follow_onboarding, default: false
  preference :hide_activity_onboarding, default: false
  preference :hide_getting_started_onboarding, default: false
  preference :hide_updates_by_you_onboarding, default: false
  preference :hide_comments_onboarding, default: false
  
  SHARING_PREFERENCES = %w(share_observations_on_facebook share_observations_on_twitter)
  NOTIFICATION_PREFERENCES = %w(comment_email_notification identification_email_notification 
    mention_email_notification message_email_notification
    project_invitation_email_notification project_journal_post_email_notification)
  
  belongs_to :life_list, :dependent => :destroy
  has_many  :provider_authorizations, :dependent => :delete_all
  has_one  :flickr_identity, :dependent => :delete
  # has_one  :picasa_identity, :dependent => :delete
  has_one  :soundcloud_identity, :dependent => :delete
  has_many :observations, :dependent => :destroy
  has_many :deleted_observations
  has_many :deleted_photos
  
  # Some interesting ways to map self-referential relationships in rails
  has_many :friendships, :dependent => :destroy
  has_many :friends, :through => :friendships
  has_many :stalkerships, :class_name => 'Friendship', :foreign_key => 'friend_id', :dependent => :destroy
  has_many :followers, :through => :stalkerships,  :source => 'user'
  
  has_many :lists, :dependent => :destroy
  has_many :life_lists
  has_many :identifications, :dependent => :destroy
  has_many :identifications_for_others,
    -> { where("identifications.user_id != observations.user_id AND identifications.current = true").
         joins(:observation) }, :class_name => "Identification"
  has_many :photos, :dependent => :destroy
  has_many :posts #, :dependent => :destroy
  has_many :journal_posts, :class_name => "Post", :as => :parent, :dependent => :destroy
  has_many :trips, -> { where("posts.type = 'Trip'") }, :class_name => "Post", :foreign_key => "user_id"
  has_many :taxon_links, :dependent => :nullify
  has_many :comments, :dependent => :destroy
  has_many :projects
  has_many :project_users, :dependent => :destroy
  has_many :project_user_invitations, :dependent => :nullify
  has_many :project_user_invitations_received, :dependent => :delete_all, :class_name => "ProjectUserInvitation"
  has_many :listed_taxa, :dependent => :nullify
  has_many :invites, :dependent => :nullify
  has_many :quality_metrics, :dependent => :destroy
  has_many :sources, :dependent => :nullify
  has_many :places, :dependent => :nullify
  has_many :messages, :dependent => :destroy
  has_many :delivered_messages, -> { where("messages.from_user_id != messages.user_id") }, :class_name => "Message", :foreign_key => "from_user_id"
  has_many :guides, :dependent => :destroy, :inverse_of => :user
  has_many :observation_fields, :dependent => :nullify, :inverse_of => :user
  has_many :observation_field_values, :dependent => :nullify, :inverse_of => :user
  has_many :updated_observation_field_values, :dependent => :nullify, :inverse_of => :updater, :foreign_key => "updater_id", :class_name => "ObservationFieldValue"
  has_many :guide_users, :inverse_of => :user, :dependent => :delete_all
  has_many :editing_guides, :through => :guide_users, :source => :guide
  has_many :created_guide_sections, :class_name => "GuideSection", :foreign_key => "creator_id", :inverse_of => :creator, :dependent => :nullify
  has_many :updated_guide_sections, :class_name => "GuideSection", :foreign_key => "updater_id", :inverse_of => :updater, :dependent => :nullify
  
  has_attached_file :icon,
    :styles => { :original => "2048x2048>", :medium => "300x300>", :thumb => "48x48#", :mini => "16x16#" },
    :processors => [:deanimator],
    :path => ":rails_root/public/attachments/:class/:attachment/:id-:style.:icon_type_extension",
    :url => "#{ CONFIG.attachments_host }/attachments/:class/:attachment/:id-:style.:icon_type_extension",
    :default_url => "/attachment_defaults/:class/:attachment/defaults/:style.png"

  # Roles
  has_and_belongs_to_many :roles, -> { uniq }
  
  has_subscribers
  has_many :subscriptions, :dependent => :delete_all
  has_many :updates, :foreign_key => :subscriber_id, :dependent => :delete_all
  has_many :flow_tasks
  has_many :project_observations, dependent: :nullify 
  belongs_to :site, :inverse_of => :users
  belongs_to :place, :inverse_of => :users

  before_validation :download_remote_icon, :if => :icon_url_provided?
  before_validation :strip_name, :strip_login
  before_save :set_time_zone
  before_save :whitelist_licenses
  before_save :get_lat_lon_from_ip_if_last_ip_changed
  before_create :set_locale
  after_save :update_observation_licenses
  after_save :update_photo_licenses
  after_save :update_sound_licenses
  after_save :update_observation_sites_later
  after_save :destroy_messages_by_suspended_user
  after_update :set_community_taxa_if_pref_changed
  after_create :create_default_life_list
  after_create :set_uri
  after_destroy :create_deleted_user
  
  validates_presence_of :icon_url, :if => :icon_url_provided?, :message => 'is invalid or inaccessible'
  validates_attachment_content_type :icon, :content_type => [/jpe?g/i, /png/i, /gif/i], 
    :message => "must be JPG, PNG, or GIF"

  validates_presence_of     :login
  
  MIN_LOGIN_SIZE = 3
  MAX_LOGIN_SIZE = 40
  
  # Regexes from restful_authentication
  LOGIN_PATTERN     = "[A-z][\\\w\\\-_]+"
  login_regex       = /\A#{ LOGIN_PATTERN }\z/                          # ASCII, strict
  bad_login_message = "use only letters, numbers, and -_ please.".freeze
  email_name_regex  = '[\w\.%\+\-]+'.freeze
  domain_head_regex = '(?:[A-Z0-9\-]+\.)+'.freeze
  domain_tld_regex  = '(?:[A-Z]+)'.freeze
  email_regex       = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i
  bad_email_message = "should look like an email address.".freeze
  
  validates_length_of       :login,     within: MIN_LOGIN_SIZE..MAX_LOGIN_SIZE
  validates_uniqueness_of   :login
  validates_format_of       :login,     with: login_regex, message: bad_login_message
  validates_exclusion_of    :login,     in: %w(password new edit create update delete destroy)

  validates_exclusion_of    :password,     in: %w(password)

  validates_length_of       :name,      maximum: 100, allow_blank: true

  validates_format_of       :email,     with: email_regex, message: bad_email_message, allow_blank: true
  validates_length_of       :email,     within: 6..100, allow_blank: true
  validates_length_of       :time_zone, minimum: 3, allow_nil: true
  
  scope :order_by, Proc.new { |sort_by, sort_dir|
    sort_dir ||= 'DESC'
    order("? ?", sort_by, sort_dir)
  }
  scope :curators, -> { joins(:roles).where("roles.name IN ('curator', 'admin')") }
  scope :admins, -> { joins(:roles).where("roles.name = 'admin'") }
  scope :active, -> { where("suspended_at IS NULL") }

  # only validate_presence_of email if user hasn't auth'd via a 3rd-party provider
  # you can also force skipping email validation by setting u.skip_email_validation=true before you save
  # (this option is necessary because the User is created before the associated ProviderAuthorization)
  # This is not a normal validation b/c email validation happens in Devise, which looks for this method
  def email_required?
    !(skip_email_validation || provider_authorizations.count > 0)
  end
  
  def icon_url_provided?
    !self.icon.present? && !self.icon_url.blank?
  end

  def user_icon_url
    return nil if icon.blank?
{'fraction_non_alphanumeric': 0.10709491850431448, 'fraction_numerical': 0.0038830297219558966, 'mean_word_length': 5.404973902364139, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 219, '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': '4288382', 'n_tokens_mistral': 6890, 'n_tokens_neox': 6756, 'n_words': 1489}