Merge commit '580eedcab22dc7ead82134d351ef118578359935' as 'plugins/redmine_workload'

This commit is contained in:
2023-03-24 11:34:26 +01:00
108 changed files with 6493 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'forwardable'
##
# GroupUserDummy representing a user of a group who holds all issues haven't been assigned
# to a real group member yet.
#
# @note The class name is relevant for sorting GroupUserDummy against User classes
# in alphabetical order.
#
class GroupUserDummy
include Redmine::I18n
extend Forwardable
def_delegators :group, :id, :firstname, :users
attr_reader :group
##
# @params group [Group] An instance of Group model.
#
def initialize(group:)
self.group = group
self.group_members = find_group_members
end
def wl_user_data
nil
end
def groups
[group]
end
def lastname
l(:label_assigned_to_group, value: group.lastname)
end
alias name lastname
def threshold_lowload_min
sum_up(:threshold_lowload_min)
end
def threshold_normalload_min
sum_up(:threshold_normalload_min)
end
def threshold_highload_min
sum_up(:threshold_highload_min)
end
def main_group
group
end
def main_group_id
group.id
end
def type
'Group'
end
private
attr_writer :group
attr_accessor :group_members
def sum_up(attribute)
return 0.0 unless group_members.presence
group_members.sum(&attribute.to_sym)
end
def find_group_members
WlUserData.where(user_id: group_member_ids, main_group: id)
end
def group_member_ids
users.map(&:id)
end
end

View File

@@ -0,0 +1,156 @@
# frozen_string_literal: true
##
# Summarize the workload of a whole group and integrate its members including
# the group user dummy.
#
class GroupWorkload
attr_reader :time_span, :user_workload
##
# @param users [WlUserSelection] Users given as WlUserSelection object.
# @param user_workload [UserWorkload] User workload given as UserWorkload object.
# @param time_span [Range] A time span given as Range object.
#
def initialize(users:, user_workload:, time_span:)
self.users = users
self.user_workload = user_workload
self.time_span = time_span
self.selected_groups = define_selected_groups
self.group_members = define_group_members
end
##
# Gives all aggregated data of the group and details for each user.
#
# @return [Hash(Group, UserWorkload#hours_per_user_issue_and_day)] Hash with
# results of UserWorkload#hours_per_user_issue_and_day for each group.
def by_group
selected_groups&.each_with_object({}) do |group, hash|
summary = summarize_over_group_members(group)
hash[group] = summary.merge(group_members[group])
end
end
private
attr_accessor :users, :selected_groups, :group_members
attr_writer :time_span, :user_workload
def define_selected_groups
users.groups&.selected
end
##
# Select only those group members having their main group equal to the group
# given.
#
def define_group_members
selected_groups&.each_with_object({}) do |group, hash|
hash[group] = sorted_user_workload.select { |user, _data| user.main_group_id == group.id }
end
end
##
# Sorting of users lastname and their class name in order to ensure that the
# GroupUserDummy will come first.
#
def sorted_user_workload
user_workload_with_availabilities.sort_by { |user, _data| [user.class.name, user.lastname] }.to_h
end
##
# Adds those users which are selected but not considered for the workload
# table since they have no issues assigned yet.
#
def user_workload_with_availabilities
availabilities = users.selected - assignees
availabilities.each do |user|
user_workload[user] = { total: total_availabilities_of(user) }
end
user_workload
end
def total_availabilities_of(user)
working_days = WlDateTools.working_days_in_time_span(time_span, user)
time_span.each_with_object({}) do |day, hash|
holiday = working_days.exclude?(day)
capacity = WlDayCapacity.new(assignee: user)
hash[day] = {}
hash[day][:hours] = 0.0
hash[day][:holiday] = holiday
hash[day][:lowload] = capacity.threshold_at(:lowload, holiday)
hash[day][:normalload] = capacity.threshold_at(:normalload, holiday)
hash[day][:highload] = capacity.threshold_at(:highload, holiday)
end
end
##
# Users having issues assigned and are therefore considered in user_workload
# calculation.
#
def assignees
user_workload.keys
end
def summarize_over_group_members(group)
{ overdue_hours: sum_of(:overdue_hours, group),
overdue_number: sum_of(:overdue_number, group),
unscheduled_number: sum_of(:unscheduled_number, group),
unscheduled_hours: sum_of(:unscheduled_hours, group),
total: total_of_group_members(group),
invisible: invisibles_of_group_members(group) }
end
def sum_of(key, group)
group_members[group].sum { |_member, data| data[key.to_sym] || 0 }
end
def total_of_group_members(group)
time_span.each_with_object({}) do |day, hash|
hash[day] = {}
hash[day][:hours] = hours_at(day, :total, group)
hash[day][:holiday] = holiday_at(day, :total, group)
hash[day][:lowload] = threshold_at(day, :lowload, group)
hash[day][:normalload] = threshold_at(day, :normalload, group)
hash[day][:highload] = threshold_at(day, :highload, group)
end
end
def invisibles_of_group_members(group)
invisible = time_span.each_with_object({}) do |day, hash|
hours = hours_at(day, :invisible, group)
holidays = holiday_at(day, :invisible, group)
hash[day] = {}
hash[day][:hours] = hours
hash[day][:holiday] = holidays
end
invisible.any? { |_date, data| data[:hours].positive? } ? invisible : nil
end
def hours_at(day, key, group)
group_members[group].sum { |_member, data| data.dig(key.to_sym, day, :hours) || 0 }
end
##
# Checks for holiday of group members including GroupUserDummy, who never will
# be on vacation, for a given day and returns true if all group members are in
# holiday at a given day or false if not.
#
def holiday_at(day, key, group)
values = group_members[group].map do |_member, data|
data.dig(key.to_sym, day, :holiday)
end
values.compact.all?
end
##
# Sums up threshold values per day and group but ignores GroupUserDummy.
#
def threshold_at(day, key, group)
group_members[group].sum do |member, data|
member.is_a?(User) ? (data.dig(:total, day, key.to_sym) || 0.0) : 0.0
end
end
end

View File

@@ -0,0 +1,372 @@
# frozen_string_literal: true
##
# Provides methods for building the workload table. These methods are used in
# WorkloadsController and its views.
#
class UserWorkload
include Redmine::I18n
include WlIssueQuery
include WlIssueState
attr_reader :assignees, :issues, :time_span, :today
def initialize(assignees:, time_span:, today:, issues: nil)
self.assignees = assignees
self.issues = open_issues_for_users(assignees, issues)
self.time_span = time_span
self.today = today
end
##
# Returns the hours per day in the given time span (including firstDay and
# lastDay) for each open issue of each of the given users.
# The result is returned as nested hash:
# The topmost hash takes a user object as key and returns a hash that takes
# among others a project as key.
# The projects hash takes among others an issue as key which again
# returns the day related data in another hash as returned by
# UserWorkload#hours_for_issue_per_day.
#
# @example Returned hash for a two day time span
#
# { #<User id: 12, ...> => { :overdue_hours => 0.0,
# :overdue_number => 0,
# :total => { Sat, 12 Mar 2022 => { :hours=>0.0, :holiday=>true },
# Sun, 13 Mar 2022 => { :hours=>0.0, :holiday=>true } },
# :invisible => {},
# #<Project id: 4711, ...> =>
# { :total => { Sat, 12 Mar 2022=>{:hours=>0.0, :holiday=>true},
# Sun, 13 Mar 2022=>{:hours=>0.0, :holiday=>true} },
# :overdue_hours => 0.0,
# :overdue_number => 0,
# #<Issue id: 12176, ...> => { Sat, 12 Mar 2022 => { :hours => 0.0,
# :active => true,
# :noEstimate => false,
# :holiday => true },
# Sun, 13 Mar 2022 => { :hours => 0.0,
# :active => true,
# :noEstimate => false,
# :holiday => true } } } }
#
# Additionally, the returned hash has two special keys:
# * :invisible. Returns a summary of all issues that are not visible for the
# currently logged in user.
# ´* :total. Returns a summary of all issues for the user that this hash is
# for.
# @return [Hash] Hash with all relevant data for displaying the workload table
# on user base.
def hours_per_user_issue_and_day
raise ArgumentError unless issues.is_a?(Array)
raise ArgumentError unless time_span.is_a?(Range)
raise ArgumentError unless today.is_a?(Date)
result = {}
issues.group_by(&:assigned_to).each do |assignee, issue_set|
working_days = working_days_in_time_span(assignee: assignee)
first_working_day_from_today_on = working_days.select { |day| day >= today }.min || today
cap = WlDayCapacity.new(assignee: assignee)
assignee = GroupUserDummy.new(group: assignee) if assignee.is_a? Group
unless result.key?(assignee)
result[assignee] = {
overdue_hours: 0.0,
overdue_number: 0,
unscheduled_hours: 0.0,
unscheduled_number: 0,
total: {},
invisible: {}
}
time_span.each do |day|
holiday = working_days.exclude?(day)
result[assignee][:total][day] = {
hours: 0.0,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
end
## Iterate over each issue in the array
issue_set.each do |issue|
project = issue.project
hours_for_issue = hours_for_issue_per_day(issue, cap, assignee)
remaining_estimated_hours = estimated_time_for_issue(issue)
# Add the issue to the total workload unless its overdue or unscheduled.
# @note issue_overdue? implies there is a due_date. In order to avoid
# double counting, a missing start_date will be ignored as criteria of
# beeing unscheduled.
if issue_overdue?(issue, today)
result[assignee][:overdue_hours] += hours_for_issue[first_working_day_from_today_on][:hours]
result[assignee][:overdue_number] += 1
elsif issue.due_date.nil?
result[assignee][:unscheduled_hours] += remaining_estimated_hours
result[assignee][:unscheduled_number] += 1
else
result[assignee][:total] = add_issue_info_to_summary(result[assignee][:total], hours_for_issue, assignee)
end
# If the issue is invisible, add it to the invisible issues summary.
# Otherwise, add it to the project (and its summary) to which it belongs
# to.
if issue.visible?
unless result[assignee].key?(project)
result[assignee][project] = {
total: {},
overdue_hours: 0.0,
overdue_number: 0,
unscheduled_hours: 0.0,
unscheduled_number: 0
}
time_span.each do |day|
holiday = working_days.exclude?(day)
result[assignee][project][:total][day] = {
hours: 0.0,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
end
# Add the issue to the project workload summary unless its overdue or unscheduled.
# @note issue_overdue? implies there is a due_date. In order to avoid
# double counting, a missing start_date will be ignored as criteria of
# beeing unscheduled.
if issue_overdue?(issue, today)
result[assignee][project][:overdue_hours] += hours_for_issue[first_working_day_from_today_on][:hours]
result[assignee][project][:overdue_number] += 1
elsif issue.due_date.nil?
result[assignee][project][:unscheduled_hours] += remaining_estimated_hours
result[assignee][project][:unscheduled_number] += 1
else
result[assignee][project][:total] =
add_issue_info_to_summary(result[assignee][project][:total], hours_for_issue, assignee)
end
# Add it to the issues for that project in any case.
result[assignee][project][issue] = hours_for_issue
else
unless issue_overdue?(issue, today)
result[assignee][:invisible] =
add_issue_info_to_summary(result[assignee][:invisible], hours_for_issue, assignee)
end
end
end
end
result
end
alias by_user hours_per_user_issue_and_day
private
attr_writer :assignees, :issues, :time_span, :today
##
# Returns the hours per day for the given issue. The result is only computed
# for days in the given time span. The function assumes that firstDay is
# today, so all remaining hours need to be done on or after firstDay.
# If the issue is overdue, all hours are assigned to the first working day
# after firstDay, or to firstDay itself, if it is a working day.
#
# The result is a hash taking a Date as key and returning a hash with the
# following keys:
# * :hours - the hours needed on that day
# * :active - true if the issue is active on that day, false else
# * :noEstimate - no estimated hours calculated because the issue has
# no estimate set or either start-time or end-time are not
# set.
# * :holiday - true if this is a holiday, false otherwise.
#
# @param issue [Issue] A single issue object.
# @param time_span [Range] Relevant time span.
# @param today [Date] The date of today.
#
# @return [Hash] If the given time span is empty, an empty hash is returned.
#
def hours_for_issue_per_day(issue, cap, assignee)
raise ArgumentError unless issue.is_a?(Issue)
raise ArgumentError unless time_span.is_a?(Range)
raise ArgumentError unless today.is_a?(Date)
hours_remaining = estimated_time_for_issue(issue)
working_days = working_days_in_time_span(assignee: assignee)
result = {}
# If issue is overdue and the remaining time may be estimated, all
# remaining hours are put on first working day.
if !issue.due_date.nil? && (issue.due_date < today)
# Initialize all days to inactive
time_span.each do |day|
# A day is active if it is after the issue start and before the issue due date
is_active = (day <= issue.due_date && (issue.start_date.nil? || issue.start_date >= day))
holiday = working_days.exclude?(day)
result[day] = {
hours: 0.0,
active: is_active,
noEstimate: false,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
first_working_day_after_today = WlDateTools.working_days_in_time_span(today..time_span.end, assignee).min
result[first_working_day_after_today] = {} if result[first_working_day_after_today].nil?
result[first_working_day_after_today][:hours] = hours_remaining
# If the hours needed for an issue can not be estimated, set all days
# outside the issues time to inactive, and all days within the issues time
# to active but not estimated.
elsif issue.due_date.nil? || issue.start_date.nil?
time_span.each do |day|
holiday = working_days.exclude?(day)
# Check: Is the issue is active on day?
result[day] = if (!issue.due_date.nil? && (day <= issue.due_date)) ||
(!issue.start_date.nil? && (day >= issue.start_date)) ||
(issue.start_date.nil? && issue.due_date.nil?)
{
hours: 0.0, # No estimate possible, use zero
# to make other calculations easy.
active: true,
noEstimate: true && !holiday, # On holidays, the zero hours
# are *not* estimated
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
# Issue is not active
else
{
hours: 0.0, # Not active => 0 hours to do.
active: false,
noEstimate: false,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
end
# The issue has start and end date
else
# Number of remaining working days for the issue:
remaining_time_span = [today, issue.start_date].max..issue.due_date
number_of_workdays_for_issue = WlDateTools.real_distance_in_days(remaining_time_span, assignee)
hours_per_workday = hours_remaining / number_of_workdays_for_issue.to_f
time_span.each do |day|
holiday = working_days.exclude?(day)
result[day] = if (day >= issue.start_date) && (day <= issue.due_date)
if day >= today
hours = holiday ? 0.0 : hours_per_workday
{
hours: hours,
active: true,
noEstimate: issue.estimated_hours.nil? && !holiday,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
else
{
hours: 0.0,
active: true,
noEstimate: false,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
else
{
hours: 0.0,
active: false,
noEstimate: false,
holiday: holiday,
lowload: threshold_at(cap, holiday, :lowload),
normalload: threshold_at(cap, holiday, :normalload),
highload: threshold_at(cap, holiday, :highload)
}
end
end
end
result
end
##
# Calculates the issues estimated hours weighted by its unfinished ratio.
#
# @param issue [Issue] The issue object with relevant estimated hours.
# @return [Float] The decimal number of remaining working hours.
#
#
def estimated_time_for_issue(issue)
raise ArgumentError unless issue.is_a?(Issue)
return 0.0 if issue.estimated_hours.nil?
return 0.0 if issue.children.any? && !consider_parent_issues?
issue.estimated_hours * ((100.0 - issue.done_ratio) / 100.0)
end
##
# Prepares a summary of issue infos.
#
# @param summary
# @param issue_info
#
def add_issue_info_to_summary(summary, issue_info, assignee)
summary ||= {}
time_span.each do |day|
holiday = { hours: 0.0, holiday: working_days_in_time_span(assignee: assignee).exclude?(day) }
summary[day] = holiday unless summary.key?(day)
summary[day][:hours] += issue_info[day][:hours]
end
summary
end
##
# Collects all working days within a given time span.
#
def working_days_in_time_span(assignee:, no_cache: false)
WlDateTools.working_days_in_time_span(time_span, assignee, no_cache: no_cache)
end
##
# Calculates the day and user dependent threshold value of the workload.
#
# @param cap [WlDayCapacity] An object able to calculate the workload day capacity.
# @param holiday [Boolean] Either a true or false value.
# @param key [Symbol|String] The short form of the threshold: lowload, normalload, highload.
#
def threshold_at(cap, holiday, key)
cap.threshold_at(key, holiday)
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
##
# Calculates day and user dependent workload threshold values
#
class WlDayCapacity
##
# @param assignee [User|Group|GroupUserDummy|String|Integer] Can handle several
# objects but should
# be User, Group or
# GroupUserDummy.
#
def initialize(**params)
self.assignee = params[:assignee]
end
def threshold_at(key, holiday)
return 0.0 if assignee == 'unassigned' || assignee.is_a?(Integer)
holiday ? 0.0 : user.send("threshold_#{key}_min")
end
private
attr_accessor :assignee
##
# Check what kind of assignee should be used.
#
def user
@user ||= assignee.is_a?(User) ? single_user(assignee) : group_user(assignee)
end
def single_user(assignee)
assignee.wl_user_data || WlDefaultUserData.new
end
def group_user(assignee)
GroupUserDummy.new(group: assignee)
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
##
# Holds default user related data for workload calculation.
#
class WlDefaultUserData
include WlUserDataDefaults
def threshold_lowload_min
default_attributes[:threshold_lowload_min].to_f
end
def threshold_normalload_min
default_attributes[:threshold_normalload_min].to_f
end
def threshold_highload_min
default_attributes[:threshold_highload_min].to_f
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
##
# Presenter organising groups to be used in views/workloads/_filers.erb.
#
class WlGroupSelection
##
# @param groups [Array(Group)] List of Group objects.
# @param user [User] A user object.
#
# @note params[:user] is currently used for tests only!
def initialize(**params)
self.groups = params[:groups] || []
self.user = define_user(params[:user])
end
##
# Returns selected groups when allowed to be viewed by the user.
#
# @return [Array(Group)] An array of group objects.
def selected
groups_by_params & allowed_to_display
end
##
# Prepares groups to be used as selection in filters.
#
def allowed_to_display
groups_allowed_to_display.sort_by { |group| group[:lastname] }
end
def all_group_ids
all_groups.map(&:id)
end
private
attr_accessor :user, :groups
##
# Define the current user.
#
def define_user(user)
user || User.current
end
##
# Queries the groups the user is allowed to view.
# @return [Array(Group)] List of group objects. The list is empty if the user
# is not allowed to view any group.
#
def groups_allowed_to_display
return all_groups if user.admin? || allowed_to?(:view_all_workloads)
return own_groups if allowed_to?(:view_own_group_workloads)
[]
end
def all_groups
Group.includes(users: :wl_user_data).distinct.all.to_a
end
def own_groups
user.groups.to_a
end
def groups_by_params
Group.joins(users: :wl_user_data).distinct.where(id: group_ids).to_a
end
def group_ids
groups.map(&:to_i)
end
def allowed_to?(permission)
user.allowed_to?(permission.to_sym, nil, global: true)
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
class WlNationalHoliday < ActiveRecord::Base
unloadable
validates :start, date: true
validates :end, date: true
validates :start, :end, :reason, presence: true
validate :check_datum
after_destroy :clearCache
after_save :clearCache
def check_datum
errors.add :end, :greater_than_start_date if workload_end_before_start?
end
private
def workload_end_before_start?
start && self.end && (start_changed? || end_changed?) && self.end < start
end
def clearCache
Rails.cache.clear
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
##
# Holds user related data for workload calculation.
#
class WlUserData < ActiveRecord::Base
include WlUserDataDefaults
belongs_to :user, inverse_of: :wl_user_data, optional: true
self.table_name = 'wl_user_datas'
validates :threshold_lowload_min, :threshold_normalload_min, :threshold_highload_min, presence: true
validate :selected_group
def self.own_groups(user_object = User.current)
user_object.groups
end
def update_to_defaults_when_new
return unless new_record?
update(default_attributes)
end
private
def selected_group
return if main_group.blank? || own_group?(user)
errors.add(:main_group, :inclusion)
end
def own_group?(user)
self.class.own_groups(user).pluck(:id).include? main_group
end
end

View File

@@ -0,0 +1,153 @@
# frozen_string_literal: true
##
# Presenter organising users to be used in views/workloads/_filers.erb.
#
class WlUserSelection
attr_reader :groups
##
# @param users [Array(User)] Selected user objects.
# @param group_selection [WlGroupSelection] WlGroupSelection object.
# @param user [User] A user object.
#
# @note params[:user] is currently used for tests only!
#
def initialize(**params)
self.users = params[:users] || []
self.groups = params[:group_selection]
self.selected_groups = groups&.selected
self.user = define_user(params[:user])
end
def all_selected
selected_groups | selected
end
##
# Returns selected users when allowed to be viewed by the given user.
#
# @return [Array(User)] An array of user objects.
def selected
(users_from_context & allowed_to_display) | include_current_user
end
##
# Prepares users to be used in filters
# @return [Array(User)] An array of user objects.
def allowed_to_display
users_allowed_to_display.sort_by(&:lastname)
end
def all_user_ids
all_users.map(&:id)
end
private
attr_accessor :user, :users, :selected_groups
attr_writer :groups
##
# Define the current user.
#
def define_user(user)
user || User.current
end
##
# It is expected to return the current user only if the user visits the
# workload index page but not if she hasn't selected herself in the filter
# fields afterwards.
#
def include_current_user
return [user] if users_from_context.blank?
[]
end
##
# If groups are given the method will query those users having one of the given
# groups as main group. If no groups are given the users_by_params will be
# returned instead.
#
# @return [Array(User)] An array of user objects.
#
def users_from_context
selected_users = users_of_groups | users_by_params
return users_by_params if selected_groups.blank?
selected_users.select do |user|
selected_groups.map(&:id).include? user.main_group_id
end
end
##
# Collects all users across all projects where the given user has the permission
# to view the project workload.
#
# @param [User] An optional single user object. Default: User.current.
# @return [Array(User)] Array of all users objects the current user may display.
#
def users_allowed_to_display
return all_users if user.admin? || allowed_to?(:view_all_workloads)
result = group_members_allowed_to(:view_own_group_workloads)
if result.blank?
result = allowed_to?(:view_own_workloads) ? [user] : []
end
result.flatten.uniq
end
def all_users
all = User.joins(:groups).distinct
return all.joins(:wl_user_data).active if selected_groups.present?
all.active
end
##
# Get all active users of groups where the current user has a membership.
#
# @param permission [String|Symbol] Permission name.
# @return [Array(User)] An array of user objects.
#
# @note user.groups does not return the user itself as group member!
#
def group_members_allowed_to(permission)
return [] unless allowed_to?(permission)
user.groups.map(&:users)
end
##
# Queries all active users as given by workload params.
#
# @return [Array(User)] An array of user objects.
def users_by_params
all_users.where(id: user_ids).to_a
end
##
# Collects all users belonging to selected groups if the user is still active.
#
# @return [Array(User)] An array of user objects.
#
def users_of_groups
return [] if selected_groups.blank?
result = selected_groups.map { |group| group.users.select(&:active?) }
result.flatten!
result.uniq
end
def user_ids
users.map(&:to_i)
end
def allowed_to?(permission)
user.allowed_to?(permission.to_sym, nil, global: true)
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
class WlUserVacation < ActiveRecord::Base
unloadable
belongs_to :user, inverse_of: :wl_user_vacations, optional: true
validates :date_from, date: true
validates :date_to, date: true
validates :date_from, :date_to, presence: true
validate :check_datum
after_destroy :clearCache
after_save :clearCache
def check_datum
errors.add :date_to, :greater_than_start_date if workload_end_before_start?
end
private
def workload_end_before_start?
date_from && date_to && (date_from_changed? || date_to_changed?) && date_to < date_from
end
def clearCache
Rails.cache.clear
end
end