In the previous commit I mentioned: > If I'm right, the `investment_votes` instance variable only exists to > avoid several database queries to get whether the current user has > supported each of the investments. > > However, that doesn't make much sense when only one investment is > shown. Now let's discuss the case when there are several investments, like in the investments index: * There are 10 investments per page by default * Each query takes less than a millisecond * We still make a query per investment to check whether the current user voted in a different group * AFAIK, there have been no performance tests showing these optimizations make the request to the investments index significantly faster * These optimizations make the code way more complex than it is without them Considering all these points, I'm removing the optimizations. I'm fine with adding `includes` calls to preload records and avoid N+1 queries even if there are no performance tests showing they make the application faster because the effect on the code complexity is negligible. But that's not the case here. Note we're using `defined?` instead of the `||=` operator because the `||=` operator will not serve its purpose when the result of the operation returns `false`.
430 lines
12 KiB
Ruby
430 lines
12 KiB
Ruby
class User < ApplicationRecord
|
|
include Verification
|
|
|
|
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable,
|
|
:trackable, :validatable, :omniauthable, :password_expirable, :secure_validatable,
|
|
authentication_keys: [:login]
|
|
|
|
acts_as_voter
|
|
acts_as_paranoid column: :hidden_at
|
|
include ActsAsParanoidAliases
|
|
|
|
include Graphqlable
|
|
|
|
has_one :administrator
|
|
has_one :moderator
|
|
has_one :valuator
|
|
has_one :manager
|
|
has_one :sdg_manager, class_name: "SDG::Manager", dependent: :destroy
|
|
has_one :poll_officer, class_name: "Poll::Officer"
|
|
has_one :organization
|
|
has_one :lock
|
|
has_many :flags
|
|
has_many :identities, dependent: :destroy
|
|
has_many :debates, -> { with_hidden }, foreign_key: :author_id, inverse_of: :author
|
|
has_many :proposals, -> { with_hidden }, foreign_key: :author_id, inverse_of: :author
|
|
has_many :activities
|
|
has_many :budget_investments, -> { with_hidden },
|
|
class_name: "Budget::Investment",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :comments, -> { with_hidden }, inverse_of: :user
|
|
has_many :failed_census_calls
|
|
has_many :notifications
|
|
has_many :direct_messages_sent,
|
|
class_name: "DirectMessage",
|
|
foreign_key: :sender_id,
|
|
inverse_of: :sender
|
|
has_many :direct_messages_received,
|
|
class_name: "DirectMessage",
|
|
foreign_key: :receiver_id,
|
|
inverse_of: :receiver
|
|
has_many :legislation_answers, class_name: "Legislation::Answer", dependent: :destroy, inverse_of: :user
|
|
has_many :follows
|
|
has_many :legislation_annotations,
|
|
class_name: "Legislation::Annotation",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :legislation_proposals,
|
|
class_name: "Legislation::Proposal",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :legislation_questions,
|
|
class_name: "Legislation::Question",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :polls, foreign_key: :author_id, inverse_of: :author
|
|
has_many :poll_answers,
|
|
class_name: "Poll::Answer",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :poll_pair_answers,
|
|
class_name: "Poll::PairAnswer",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :poll_partial_results,
|
|
class_name: "Poll::PartialResult",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :poll_questions,
|
|
class_name: "Poll::Question",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :poll_recounts,
|
|
class_name: "Poll::Recount",
|
|
foreign_key: :author_id,
|
|
inverse_of: :author
|
|
has_many :topics, foreign_key: :author_id, inverse_of: :author
|
|
belongs_to :geozone
|
|
|
|
validates :username, presence: true, if: :username_required?
|
|
validates :username, uniqueness: { scope: :registering_with_oauth }, if: :username_required?
|
|
validates :document_number, uniqueness: { scope: :document_type }, allow_nil: true
|
|
|
|
validate :validate_username_length
|
|
|
|
validates :official_level, inclusion: { in: 0..5 }
|
|
validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
|
|
|
|
validates_associated :organization, message: false
|
|
|
|
accepts_nested_attributes_for :organization, update_only: true
|
|
|
|
attr_accessor :skip_password_validation, :use_redeemable_code, :login
|
|
|
|
scope :administrators, -> { joins(:administrator) }
|
|
scope :moderators, -> { joins(:moderator) }
|
|
scope :organizations, -> { joins(:organization) }
|
|
scope :sdg_managers, -> { joins(:sdg_manager) }
|
|
scope :officials, -> { where("official_level > 0") }
|
|
scope :male, -> { where(gender: "male") }
|
|
scope :female, -> { where(gender: "female") }
|
|
scope :newsletter, -> { where(newsletter: true) }
|
|
scope :for_render, -> { includes(:organization) }
|
|
scope :by_document, ->(document_type, document_number) do
|
|
where(document_type: document_type, document_number: document_number)
|
|
end
|
|
scope :email_digest, -> { where(email_digest: true) }
|
|
scope :active, -> { where(erased_at: nil) }
|
|
scope :erased, -> { where.not(erased_at: nil) }
|
|
scope :public_for_api, -> { all }
|
|
scope :by_authors, ->(author_ids) { where(id: author_ids) }
|
|
scope :by_comments, ->(commentables) do
|
|
joins(:comments).where("comments.commentable": commentables).distinct
|
|
end
|
|
scope :by_username_email_or_document_number, ->(search_string) do
|
|
search = "%#{search_string.strip}%"
|
|
where("username ILIKE ? OR email ILIKE ? OR document_number ILIKE ?", search, search, search)
|
|
end
|
|
scope :between_ages, ->(from, to) do
|
|
where(
|
|
"date_of_birth > ? AND date_of_birth < ?",
|
|
to.years.ago.beginning_of_year,
|
|
from.years.ago.end_of_year
|
|
)
|
|
end
|
|
|
|
before_validation :clean_document_number
|
|
|
|
# Get the existing user by email if the provider gives us a verified email.
|
|
def self.first_or_initialize_for_oauth(auth)
|
|
oauth_email = auth.info.email
|
|
oauth_email_confirmed = oauth_email.present? && (auth.info.verified || auth.info.verified_email)
|
|
oauth_user = User.find_by(email: oauth_email) if oauth_email_confirmed
|
|
|
|
oauth_user || User.new(
|
|
username: auth.info.name || auth.uid,
|
|
email: oauth_email,
|
|
oauth_email: oauth_email,
|
|
password: Devise.friendly_token[0, 20],
|
|
terms_of_service: "1",
|
|
confirmed_at: oauth_email_confirmed ? DateTime.current : nil
|
|
)
|
|
end
|
|
|
|
def name
|
|
organization? ? organization.name : username
|
|
end
|
|
|
|
def debate_votes(debates)
|
|
voted = votes.for_debates(Array(debates).map(&:id))
|
|
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
|
|
end
|
|
|
|
def proposal_votes(proposals)
|
|
voted = votes.for_proposals(Array(proposals).map(&:id))
|
|
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
|
|
end
|
|
|
|
def legislation_proposal_votes(proposals)
|
|
voted = votes.for_legislation_proposals(proposals)
|
|
voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value }
|
|
end
|
|
|
|
def comment_flags(comments)
|
|
comment_flags = flags.for_comments(comments)
|
|
comment_flags.each_with_object({}) { |f, h| h[f.flaggable_id] = true }
|
|
end
|
|
|
|
def voted_in_group?(group)
|
|
votes.for_budget_investments(Budget::Investment.where(group: group)).exists?
|
|
end
|
|
|
|
def headings_voted_within_group(group)
|
|
Budget::Heading.where(id: voted_investments.by_group(group).pluck(:heading_id))
|
|
end
|
|
|
|
def voted_investments
|
|
Budget::Investment.where(id: votes.for_budget_investments.pluck(:votable_id))
|
|
end
|
|
|
|
def administrator?
|
|
administrator.present?
|
|
end
|
|
|
|
def moderator?
|
|
moderator.present?
|
|
end
|
|
|
|
def valuator?
|
|
valuator.present?
|
|
end
|
|
|
|
def manager?
|
|
manager.present?
|
|
end
|
|
|
|
def sdg_manager?
|
|
sdg_manager.present?
|
|
end
|
|
|
|
def poll_officer?
|
|
poll_officer.present?
|
|
end
|
|
|
|
def organization?
|
|
organization.present?
|
|
end
|
|
|
|
def verified_organization?
|
|
organization&.verified?
|
|
end
|
|
|
|
def official?
|
|
official_level && official_level > 0
|
|
end
|
|
|
|
def add_official_position!(position, level)
|
|
return if position.blank? || level.blank?
|
|
|
|
update! official_position: position, official_level: level.to_i
|
|
end
|
|
|
|
def remove_official_position!
|
|
update! official_position: nil, official_level: 0
|
|
end
|
|
|
|
def has_official_email?
|
|
domain = Setting["email_domain_for_officials"]
|
|
email.present? && ((email.end_with? "@#{domain}") || (email.end_with? ".#{domain}"))
|
|
end
|
|
|
|
def display_official_position_badge?
|
|
return true if official_level > 1
|
|
|
|
official_position_badge? && official_level == 1
|
|
end
|
|
|
|
def block
|
|
hide
|
|
|
|
Debate.hide_all debate_ids
|
|
Comment.hide_all comment_ids
|
|
Proposal.hide_all proposal_ids
|
|
Budget::Investment.hide_all budget_investment_ids
|
|
ProposalNotification.hide_all ProposalNotification.where(author_id: id).pluck(:id)
|
|
end
|
|
|
|
def full_restore
|
|
ActiveRecord::Base.transaction do
|
|
Debate.restore_all debates.where("hidden_at >= ?", hidden_at)
|
|
Comment.restore_all comments.where("hidden_at >= ?", hidden_at)
|
|
Proposal.restore_all proposals.where("hidden_at >= ?", hidden_at)
|
|
Budget::Investment.restore_all budget_investments.where("hidden_at >= ?", hidden_at)
|
|
ProposalNotification.restore_all(
|
|
ProposalNotification.only_hidden.where("hidden_at >= ?", hidden_at).where(author_id: id)
|
|
)
|
|
|
|
restore
|
|
end
|
|
end
|
|
|
|
def erase(erase_reason = nil)
|
|
update!(
|
|
erased_at: Time.current,
|
|
erase_reason: erase_reason,
|
|
username: nil,
|
|
email: nil,
|
|
unconfirmed_email: nil,
|
|
phone_number: nil,
|
|
encrypted_password: "",
|
|
confirmation_token: nil,
|
|
reset_password_token: nil,
|
|
email_verification_token: nil,
|
|
confirmed_phone: nil,
|
|
unconfirmed_phone: nil
|
|
)
|
|
identities.destroy_all
|
|
end
|
|
|
|
def erased?
|
|
erased_at.present?
|
|
end
|
|
|
|
def take_votes_if_erased_document(document_number, document_type)
|
|
erased_user = User.erased.find_by(document_number: document_number,
|
|
document_type: document_type)
|
|
if erased_user.present?
|
|
take_votes_from(erased_user)
|
|
erased_user.update!(document_number: nil, document_type: nil)
|
|
end
|
|
end
|
|
|
|
def take_votes_from(other_user)
|
|
return if other_user.blank?
|
|
|
|
Poll::Voter.where(user_id: other_user.id).update_all(user_id: id)
|
|
Budget::Ballot.where(user_id: other_user.id).update_all(user_id: id)
|
|
Vote.where("voter_id = ? AND voter_type = ?", other_user.id, "User").update_all(voter_id: id)
|
|
data_log = "id: #{other_user.id} - #{Time.current.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
update!(former_users_data_log: "#{former_users_data_log} | #{data_log}")
|
|
end
|
|
|
|
def locked?
|
|
Lock.find_or_create_by!(user: self).locked?
|
|
end
|
|
|
|
def self.search(term)
|
|
return none if term.blank?
|
|
|
|
search = term.strip
|
|
where("email = ? OR username ILIKE ?", search, "%#{search}%")
|
|
end
|
|
|
|
def self.username_max_length
|
|
@username_max_length ||= columns.find { |c| c.name == "username" }.limit || 60
|
|
end
|
|
|
|
def self.minimum_required_age
|
|
(Setting["min_age_to_participate"] || 16).to_i
|
|
end
|
|
|
|
def show_welcome_screen?
|
|
verification = Setting["feature.user.skip_verification"].present? ? true : unverified?
|
|
sign_in_count == 1 && verification && !organization && !administrator?
|
|
end
|
|
|
|
def password_required?
|
|
return false if skip_password_validation
|
|
|
|
super
|
|
end
|
|
|
|
def username_required?
|
|
!organization? && !erased?
|
|
end
|
|
|
|
def email_required?
|
|
!erased? && unverified?
|
|
end
|
|
|
|
def locale
|
|
self[:locale] ||= I18n.default_locale.to_s
|
|
end
|
|
|
|
def confirmation_required?
|
|
super && !registering_with_oauth
|
|
end
|
|
|
|
def send_oauth_confirmation_instructions
|
|
if oauth_email != email
|
|
update(confirmed_at: nil)
|
|
send_confirmation_instructions
|
|
end
|
|
update(oauth_email: nil) if oauth_email.present?
|
|
end
|
|
|
|
def name_and_email
|
|
"#{name} (#{email})"
|
|
end
|
|
|
|
def age
|
|
Age.in_years(date_of_birth)
|
|
end
|
|
|
|
def save_requiring_finish_signup
|
|
begin
|
|
self.registering_with_oauth = true
|
|
save!(validate: false)
|
|
# Devise puts unique constraints for the email the db, so we must detect & handle that
|
|
rescue ActiveRecord::RecordNotUnique
|
|
self.email = nil
|
|
save!(validate: false)
|
|
end
|
|
true
|
|
end
|
|
|
|
def ability
|
|
@ability ||= Ability.new(self)
|
|
end
|
|
delegate :can?, :cannot?, to: :ability
|
|
|
|
def public_proposals
|
|
public_activity? ? proposals : User.none
|
|
end
|
|
|
|
def public_debates
|
|
public_activity? ? debates : User.none
|
|
end
|
|
|
|
def public_comments
|
|
public_activity? ? comments : User.none
|
|
end
|
|
|
|
# overwritting of Devise method to allow login using email OR username
|
|
def self.find_for_database_authentication(warden_conditions)
|
|
conditions = warden_conditions.dup
|
|
login = conditions.delete(:login)
|
|
where(conditions.to_hash).find_by(["lower(email) = ?", login.downcase]) ||
|
|
where(conditions.to_hash).find_by(["username = ?", login])
|
|
end
|
|
|
|
def self.find_by_manager_login(manager_login)
|
|
find_by(id: manager_login.split("_").last)
|
|
end
|
|
|
|
def interests
|
|
followables = follows.map(&:followable)
|
|
followables.compact.map { |followable| followable.tags.map(&:name) }.flatten.compact.uniq
|
|
end
|
|
|
|
def send_devise_notification(notification, *args)
|
|
devise_mailer.send(notification, self, *args).deliver_later
|
|
end
|
|
|
|
private
|
|
|
|
def clean_document_number
|
|
return unless document_number.present?
|
|
|
|
self.document_number = document_number.gsub(/[^a-z0-9]+/i, "").upcase
|
|
end
|
|
|
|
def validate_username_length
|
|
validator = ActiveModel::Validations::LengthValidator.new(
|
|
attributes: :username,
|
|
maximum: User.username_max_length)
|
|
validator.validate(self)
|
|
end
|
|
end
|