Files
nairobi/app/models/user.rb
Javi Martín 1d85a63e7c Calculate age stats based on the participation date
We were calculating the age stats based on the age of the users who
participated... at the moment where we were calculating the stats. That
means that, if 20 years ago, 1000 people who were 16 years old
participated, they would be shown as having 36 years in the stats.

Instead, we want to show the stats at the time when the process took
place, so we're implementing a `participation_date` method.

Note that, for polls, we could actually use the `age` column in the
`poll_voters` table. However, doing so would be harder, would only work
for polls but not for budgets, and it wouldn't be statistically very
relevant, since the stats are shown by age groups, and only a small
percentage of people would change their age group (and only to the
nearest one) between the time they participate and the time the process
ends.

We might use the `poll_voters` table in the future, though, since we
have a similar issue with geozones and genders, and using the
information in `poll_voters` would solve it as well (only for polls,
though).

Also note that we're using the `ends_at` dates because some people but
be too young to vote when a process starts but old enough to vote when
the process ends.

Finally, note that we might need to change the way we calculate the
participation date for a budget, since some budgets might not enabled
every phase. Not sure how stats work in that scenario (even before these
changes).
2024-05-13 15:42:37 +02:00

460 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
where(
"date_of_birth > ? AND date_of_birth < ?",
(at_time - to.years).beginning_of_year,
(at_time - from.years).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_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] || I18n.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