app/models/user.rb
class User < ApplicationRecord
# Virtual attribute, which can be used in member lists to add a note in memory when the user
# has joined a group or list.
#
# See:
# - `_member_list.html.haml`
#
attr_accessor :member_since
# Gamification: https://github.com/merit-gem/merit
include Merit
has_merit
validates_presence_of :last_name
validates_format_of :first_name, with: /\A[^\,]*\z/, if: Proc.new { |user| user.first_name.present? } # The name must not contain a comma.
validates_format_of :last_name, with: /\A[^\,]*\z/
before_validation :strip_first_and_last_name
before_validation :change_alias_if_already_taken
validates_uniqueness_of :alias, :if => Proc.new { |user| user.account and user.alias.present? }
validates_format_of :email, :with => Devise::email_regexp, :if => Proc.new { |user| user.email.present? }, judge: :ignore
has_one :account, class_name: "UserAccount", autosave: true, inverse_of: :user, dependent: :destroy
validates_associated :account
scope :with_account, -> { joins(:account) }
delegate :send_welcome_email, :to => :account
has_dag_links ancestor_class_names: %w(Page Group Event), descendant_class_names: %w(Page Post), link_class_name: 'DagLink'
has_many :relationships_as_first_user, foreign_key: 'user1_id', class_name: "Relationship", dependent: :destroy, inverse_of: :user1
has_many :relationships_as_second_user, foreign_key: 'user2_id', class_name: "Relationship", dependent: :destroy, inverse_of: :user2
has_many :bookmarks
has_many :last_seen_activities
has_many :comments, foreign_key: 'author_user_id', class_name: 'Comment'
has_many :mentions, foreign_key: 'whom_user_id', class_name: 'Mention'
scope :regular, -> { alive }
include Structureable
include Navable
before_save :generate_alias_if_necessary, :capitalize_name
# Easy user settings: https://github.com/huacnlee/rails-settings-cached
# For example:
#
# user = User.find(123)
# user.settings.color = :red
# user.settings.color # => :red
#
include RailsSettings::Extend
# Mixins
# ==========================================================================================
include UserName
include UserGraph
include UserMixins::Memberships
include UserMixins::Identification
include UserCorporations
include UserGroups
include UserEvents
include UserStatus
include UserProfile
include UserDateOfBirth
include UserAvatar
include UserRoles
include UserNotifications
include UserPosts
include UserMerit
include UserRecommendations
include UserOmniauth
include UserSearch
include UserContacts
include UserVcfExport
include UserDocuments
include UserGeoSearch
include UserLocation
include UserPostalSubscriptions
include UserGender
include UserBio
include UserBackup
include UserCouleur
def as_json(*options)
super.merge({
title: title,
avatar_path: avatar_path,
avatar_url: avatar_url
})
end
# General Properties
# ==========================================================================================
# For printed registers, a summary string is useful.
#
# For example: "Wein, Björn (Gi 06, Dp 08), *19.12.1980, Feinhäuser Allee 25, 35037 Marburg, 06421-12345, wein@example.com"
#
def summary_string
summary_components.values.select(&:present?).join(", ")
.gsub(", (", " (")
.gsub(" ()", "")
.gsub(", , ,", ",")
end
def summary_components
{
last_name: last_name,
first_name: first_name,
name_affix: "(#{name_affix})",
date_of_birth: "*#{localized_date_of_birth}",
academic_degree: academic_degree,
employment_title: employment_title,
address: primary_address.gsub("\n", ", "),
phone: phone,
email: email
}
end
# This sets the format of the User urls to be
#
# example.com/users/24-john-doe
#
# rather than just
#
# example.com/users/24
#
# This method uses a cache on purpose, since it is directly used by rails
# to construct the url.
#
def to_param
"#{id} #{title}".parameterize
end
# The preferred locale of the user, which can be set through
# the user settings or the page footer.
#
# In order to suppress the default value and just to read the
# setting from the database, call `user.locale(true)`.
#
def locale(no_default = false)
if no_default
super()
else
super() || Setting.preferred_locale || I18n.default_locale
end
end
def timezone
# TODO: Implement a setting where the user can choose his own time zone.
# See: http://railscasts.com/episodes/106-time-zones-revised
User.default_timezone
end
def time_zone
timezone
end
def self.default_timezone
AppVersion.default_timezone
end
def self.default_time_zone
default_timezone
end
def ability
@ability ||= Ability.new(self)
end
def can?(what, with_whom)
ability.can? what, with_whom
end
# Date of Death
# The date of death is localized already!
# Why?
#
def date_of_death
profile_fields.where(label: 'date_of_death').first.try(:value)
end
def set_date_of_death_if_unset(new_date_of_death)
new_date_of_death = I18n.localize(new_date_of_death.to_date)
unless self.date_of_death
profile_fields.create(type: "ProfileFields::General", label: 'date_of_death', value: new_date_of_death)
end
end
def dead?
date_of_death ? true : false
end
def alive?
not dead?
end
# Example:
#
# user.mark_as_deceased at: "2014-03-05".to_datetime
#
def mark_as_deceased(options = {})
date = options[:at] || Time.zone.now
self.current_corporations.each do |corporation|
self.current_status_membership_in(corporation).move_to corporation.deceased, at: date
end
end_all_non_corporation_memberships at: date
set_date_of_death_if_unset(date)
account.try(:destroy)
end
# Defines whether the user can be marked as deceased (by a workflow).
#
def markable_as_deceased?
alive?
end
def end_all_non_corporation_memberships(options = {})
date = options[:at] || Time.zone.now
for group in (self.direct_groups - Group.corporations_parent.descendant_groups)
Membership.find_by_user_and_group(self, group).invalidate at: date
end
end
def postal_address_with_name_surrounding
address_label.to_s
end
def name_with_surrounding
(
name_surrounding_profile_field.try(:text_above_name).to_s + "\n" +
"#{name_surrounding_profile_field.try(:name_prefix)} #{name} #{name_surrounding_profile_field.try(:name_suffix)}".strip + "\n" +
name_surrounding_profile_field.try(:text_below_name).to_s
).strip
end
def address_label
AddressLabel.new(self.name, self.postal_address_field_or_first_address_field,
self.name_surrounding_profile_field, self.personal_title, self.corporation_name)
end
# Associated Objects
# ==========================================================================================
# Alias
# ------------------------------------------------------------------------------------------
# The UserAlias class inherits from String, but has some more methods, e.g. a method
# to generate a new alias from other user attributes. To make sure that `alias` returns
# an object of UserAlias type, the accessor methods are overridden here.
#
def alias
UserAlias.new(super) if super.present?
end
def generate_alias
UserAlias.generate_for(self)
end
def generate_alias!
self.alias = self.generate_alias
end
def generate_alias_if_necessary
self.generate_alias! if self.account and self.alias.blank?
end
private :generate_alias_if_necessary
def change_alias_if_already_taken
if self.has_account? && self.alias.present? && User.where(alias: self.alias).count > 1
self.generate_alias!
end
end
# User Account
# ------------------------------------------------------------------------------------------
# A user stored in the database does not necessarily possess the right to log in, i.e.
# have a user account. This method allows to find out whether the user has got an
# active user account.
#
def has_account?
return true if self.account
return false
end
def has_no_account?
not self.account.present?
end
def guest_user?
has_no_account?
end
# This method activates the user account, i.e. grants the user the right to log in.
#
def activate_account
create_account unless account
end
# This method deactivates the user account, i.e. destroys the associated object
# and prevents the user from logging in.
#
def deactivate_account
raise ActiveRecord::RecordNotFound, "no user account exists, therefore it can't be destroyed." if not self.account
self.account.destroy
self.account = nil
end
def token
account.try(:auth_token)
end
# Activities
# ------------------------------------------------------------------------------------------
def find_or_build_last_seen_activity
last_seen_activities.last || last_seen_activities.build
end
def update_last_seen_activity(description = nil, object = nil)
unless readonly?
if description and not self.incognito?
activity = find_or_build_last_seen_activity
activity.touch unless activity.new_record? # even if the attributes didn't change. The user probably hit 'reload' then.
activity.description = description
activity.link_to_object = object
activity.save
else
last_seen_activities.destroy_all
end
end
end
# Corporations
# ------------------------------------------------------------------------------------------
# This returns all corporations of the user. The Corporation model inherits from the Group
# model. corporations are child_groups of the corporations_parent_group in the DAG.
#
# everyone
# |----- corporations_parent
# | |---------- corporation_a <----
# | | |--- ... |--- These are corporations
# | |---------- corporation_b <----
# | |--- ...
# |----- other_group_1
# |----- other_group_2
#
# Warning!
# This method does not distinguish between regular members and guest members.
# If a user is only guest in a corporation, `user.corporations` WILL list this corporation.
#
def corporations(options = {})
return corporations_with_past if options[:with_invalid]
groups.where(type: 'Corporation')
end
def corporations_with_past
ancestor_groups.where(type: 'Corporation')
end
# This returns the corporations the user is currently member of.
#
def current_corporations
self.corporations.select do |corporation|
Role.of(self).in(corporation).current_member?
end || []
end
# This returns the same as `current_corporations`, but sorted by the
# date of joining the corporations, earliest joining first.
#
def sorted_current_corporations
current_corporations.sort_by do |corporation|
Membership.with_invalid.find_by_user_and_group(self, corporation).valid_from || Time.zone.now - 100.years
end
end
# This returns the first corporation where the user is still member of or nil
#
def first_corporation
# if self.corporations
# self.corporations.select do |corporation|
# not ( self.guest_of?( corporation )) and
# not ( self.former_member_of_corporation?( corporation ))
# end.sort_by do |corporation|
# corporation.membership_of( self ).valid_from or Time.zone.now
# end.first
# end
sorted_current_corporations.first
end
# The primary corporation is the one the user is most associated with.
#
# user.primary_corporation
# user.primary_corporation at: 2.years.ago
#
def primary_corporation(options = {})
if options[:at]
memberships.with_past.where(ancestor_id: Group.flagged(:full_members).pluck(:id))
.at_time(options[:at]).order(:valid_from)
.first.try(:group).try(:corporation)
else
# Temporary hack. This might not be correct for all cases.
first_corporation
end
end
# This returns the groups within the first corporation
# where the user is still member of in the order of entering the group.
# The groups must not be special and the user most not be a special member.
def my_groups_in_first_corporation
if first_corporation
self.groups.select do |group|
group.ancestor_groups.include?(self.first_corporation) and
not group.is_special_group? and
not self.guest_of?(group)
end
else
Group.none
end
end
def last_group_in_first_corporation
my_groups_in_first_corporation.last
end
# Corporate Vita
# ==========================================================================================
def corporate_vita_memberships_in(corporation)
Memberships::Status.find_all_by_user_and_corporation(self, corporation).with_past.where(direct: true)
end
def apply_gap_correction
corporations_with_past.collect do |corporation|
Memberships::Status.apply_gap_correction self, corporation, membership_type: "Memberships::Status"
end
end
# Relationships
# ------------------------------------------------------------------------------------------
# This returns all relationship opjects.
#
def relationships
relationships_as_first_user + relationships_as_second_user
end
# Workflows
# ------------------------------------------------------------------------------------------
# This method returns all workflows applicable for this user, i.e. this returns
# all workflows of all groups the user is a member of.
#
def workflows
my_workflows = []
self.groups.each do |group|
my_workflows += group.child_workflows
end
return my_workflows
end
def workflows_for(group)
(([group.becomes(Group)] + group.descendant_groups) & self.groups)
.collect { |g| g.child_workflows }.select { |w| not w.nil? }.flatten.uniq
end
def workflows_by_corporation
hash = {}
other_workflows = self.workflows
self.corporations.each do |corporation|
corporation_workflows = self.workflows_for(corporation)
hash.merge!( corporation.title.to_s => corporation_workflows )
other_workflows -= corporation_workflows
end
hash.merge!( I18n.t(:others).to_s => other_workflows ) if other_workflows.count > 0
return hash
end
# News Entries (Pages)
# -------------------
# List news (Pages) that concern the user.
#
# everyone ---- page_1 ---- page_2 <--- show
# |
# |----- group_1 ---- page_3 <--- DO NOT show
# |
# |----- group_2 ---- user
# | |-- page_4 <--- show
# |
# |--- user
#
def news_pages
# List all pages that do not have ancestor groups
# which the user is no member of.
#
Page.where.not(id: Page.joins(:ancestor_groups).where.not(groups: {id: self.groups}))
end
# Bookmarked Objects
# ------------------------------------------------------------------------------------------
# This method lists all bookmarked objets of this user.
#
def bookmarked_objects
self.bookmarks.collect { |bookmark| bookmark.bookmarkable }
end
# Hidden
# ==========================================================================================
#
# Some users are hidden for regular users. They can only be seen by their administrators.
# This is necessary for some organizations due to privacy reasons.
#
def hidden?
self.hidden
end
def hidden
self.member_of? Group.hidden_users
end
def hidden=(hidden)
Group.hidden_users.assign_user self if hidden == true || hidden == "true"
Group.hidden_users.unassign_user self if hidden == false || hidden == "false"
end
def self.find_all_hidden
self.where(id: Group.hidden_users.member_ids)
end
def self.find_all_non_hidden
self.where.not(id: Group.hidden_users.member_ids)
end
# Group Flags
# ==========================================================================================
# This efficiently returns all flags of the groups the user is currently in.
#
# For example, ony can find out with one sql query whether a user is hidden:
#
# user.group_flags.include? 'hidden_users'
#
def group_flags
groups.joins(:flags).pluck('flags.key')
end
# Finder Methods
# ==========================================================================================
# This finds a user matching an auth token.
#
def self.find_by_token(token)
UserAccount.where(auth_token: token).limit(1).first.try(:user)
end
# This method finds all users having the given email attribute.
# notice: case insensitive
#
def self.find_all_by_email( email ) # TODO: Test this; # TODO: optimize using where
email_fields = ProfileField.where( type: "ProfileFields::Email", value: email )
matching_users = email_fields
.select{ |ef| ef.profileable_type == "User" }
.collect { |ef| ef.profileable }
return matching_users.to_a
end
def self.find_by_email( email )
self.find_all_by_email(email).first
end
def self.with_group_flags
self.joins(:groups => :flags)
end
def self.with_group_flag(flag)
self.with_group_flags.where("flags.key = ?", flag)
end
def self.hidden
self.with_group_flag('hidden_users')
end
def self.deceased
self.joins(:profile_fields).where(:profile_fields => {label: 'date_of_death'})
end
def self.alive
self.where.not id: deceased
end
def self.without_account
self.includes(:account).where(:user_accounts => { :user_id => nil })
end
def self.with_email
self.joins(:profile_fields).where('profile_fields.type = ? AND profile_fields.value != ?', 'ProfileFields::Email', '')
end
def self.applicable_for_new_account
self.alive.without_account.with_email
end
def self.joins_groups
self.joins(:groups).where('dag_links.valid_to IS NULL')
end
def accept_terms(terms_stamp)
self.accepted_terms = terms_stamp
self.accepted_terms_at = Time.zone.now
save!
end
def accepted_terms?(terms_stamp)
self.accepted_terms == terms_stamp
end
def self.apply_filter(filter)
if filter && filter.include?("without_email")
self.without_email
elsif filter && filter.include?("with_local_postal_mail_subscription")
self.with_local_postal_mail_subscription
else
self.all
end
end
def self.without_email
ids = self.select { |user| not user.email.present? or user.email_needs_review? }.map(&:id)
where(id: ids)
end
# Helpers
# ==========================================================================================
# The string returned by this method represents the user in the rails console.
# For example, if you type `User.all` in the console, the answer would be:
#
# User: alias_of_the_first_user, User: alias_of_the_second_user, ...
#
def inspect
"User: #{self.id} #{self.alias}"
end
# ==========================================================================================
def parent
status_group_in_primary_corporation || direct_groups.first
end
include UserCaching if use_caching?
end