Files
grecia/app/models/user.rb
Javi Martín 6de4737b70 Allow different default locales per tenant
Note that, for everything to work consistently, we need to make sure
that the default locale is one of the available locales.

Also note that we aren't overwriting the `#save ` method set by
globalize. I didn't feel too comfortable changing a monkey-patch which
ideally shouldn't be there in the first place, I haven't found a case
where `Globalize.locale` is `nil` (since it defaults to `I18n.locale`,
which should never be `nil`), so using `I18n.default_locale` probably
doesn't affect us.
2024-06-05 16:10:56 +02:00

459 lines
14 KiB
Ruby

class User < ApplicationRecord
include Verification
attribute :registering_from_web, default: false
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable,
:trackable, :validatable, :omniauthable, :password_expirable, :secure_validatable,
authentication_keys: [:login]
devise :lockable if Rails.application.config.devise_lockable
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 :related_contents, foreign_key: :author_id, inverse_of: :author, dependent: nil
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, at_time: Time.current) do
start_date = at_time - (to + 1).years + 1.day
end_date = at_time - from.years
where(date_of_birth: start_date.beginning_of_day..end_date.end_of_day)
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_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified
oauth_email_confirmed = oauth_email.present? && oauth_verified
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 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.where(votable: 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.where(votable: Budget::Investment.all).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
Legislation::Proposal.hide_all legislation_proposal_ids
Proposal.hide_all proposal_ids
Budget::Investment.hide_all budget_investment_ids
ProposalNotification.hide_all ProposalNotification.where(author_id: id).ids
remove_roles
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)
Legislation::Proposal.restore_all legislation_proposals.only_hidden.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
remove_roles
end
def erased?
erased_at.present?
end
def remove_roles
administrator&.destroy!
valuator&.destroy!
moderator&.destroy!
manager&.destroy!
sdg_manager&.destroy!
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? || registering_from_web)
end
def locale
self[:locale] || Setting.default_locale.to_s
end
def confirmation_required?
super && !registering_with_oauth
end
def send_oauth_confirmation_instructions
if oauth_email != email || confirmed_at.nil?
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 : proposals.none
end
def public_debates
public_activity? ? debates : debates.none
end
def public_comments
public_activity? ? comments : comments.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, *)
devise_mailer.send(notification, self, *).deliver_later
end
def add_subscriptions_token
update!(subscriptions_token: SecureRandom.base58(32)) if subscriptions_token.blank?
end
def self.password_complexity
if Tenant.current_secrets.dig(:security, :password_complexity)
{ digit: 1, lower: 1, symbol: 0, upper: 1 }
else
{ digit: 0, lower: 0, symbol: 0, upper: 0 }
end
end
def self.maximum_attempts
(Tenant.current_secrets.dig(:security, :lockable, :maximum_attempts) || 20).to_i
end
def self.unlock_in
(Tenant.current_secrets.dig(:security, :lockable, :unlock_in) || 1).to_f.hours
end
def to_param
"#{id}-#{slug}"
end
def slug
username.to_s.parameterize
end
private
def clean_document_number
return if document_number.blank?
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