Merge commit '580eedcab22dc7ead82134d351ef118578359935' as 'plugins/redmine_workload'
This commit is contained in:
106
plugins/redmine_workload/.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
106
plugins/redmine_workload/.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
assignees:
|
||||
- liaham
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Contact Details
|
||||
description: How can we get in touch with you if we need more info?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: What did you expect?
|
||||
description: Please tell us, what did you expect to happen?
|
||||
placeholder: Tell us what should have happend!
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: What has happend instead?
|
||||
description: Please tell us, what has happend instead?
|
||||
placeholder: Tell us what has happend!
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: possible-solution
|
||||
attributes:
|
||||
label: What could be a possible solution?
|
||||
description: Please tell us, how do you think the problem could be solved?
|
||||
placeholder: Tell us what is your idea for a possible solution!
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How can we reproduce the problem?
|
||||
description: Please tell us, what steps to execute to reproduce the issue.
|
||||
placeholder: Tell us step by step what we need to do!
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: In what environment are you running the plugin?
|
||||
description: Please copy and paste your environment information as displayed in Administration » Information or run `bin/about` in the root dir of your Redmine instance.
|
||||
placeholder: Paste your environment information here!
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our plugin are you running?
|
||||
options:
|
||||
- 1.0.0
|
||||
- 1.0.1
|
||||
- 1.0.2
|
||||
- 1.0.3
|
||||
- 1.1.0
|
||||
- 2.0.0
|
||||
- 2.0.1
|
||||
- 2.0.2
|
||||
- 2.1.0
|
||||
- 2.2.0
|
||||
- 2.2.1 (Latest)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://circle.xmera.de/projects/contributors-guide/wiki/Code-of-conduct)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
27
plugins/redmine_workload/.gitignore
vendored
Normal file
27
plugins/redmine_workload/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/.project
|
||||
/.loadpath
|
||||
/config/additional_environment.rb
|
||||
/config/configuration.yml
|
||||
/config/database.yml
|
||||
/config/email.yml
|
||||
/config/initializers/session_store.rb
|
||||
/coverage
|
||||
/db/*.db
|
||||
/db/*.sqlite3
|
||||
/db/schema.rb
|
||||
/files/*
|
||||
/lib/redmine/scm/adapters/mercurial/redminehelper.pyc
|
||||
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
|
||||
/log/*.log*
|
||||
/log/mongrel_debug
|
||||
/public/dispatch.*
|
||||
/public/plugin_assets
|
||||
/tmp/*
|
||||
/tmp/cache/*
|
||||
/tmp/sessions/*
|
||||
/tmp/sockets/*
|
||||
/tmp/test/*
|
||||
/vendor/rails
|
||||
*.rbc
|
||||
/nbproject
|
||||
.history
|
||||
29
plugins/redmine_workload/.hgignore
Normal file
29
plugins/redmine_workload/.hgignore
Normal file
@@ -0,0 +1,29 @@
|
||||
syntax: glob
|
||||
|
||||
.project
|
||||
.loadpath
|
||||
config/additional_environment.rb
|
||||
config/configuration.yml
|
||||
config/database.yml
|
||||
config/email.yml
|
||||
config/initializers/session_store.rb
|
||||
coverage
|
||||
db/*.db
|
||||
db/*.sqlite3
|
||||
db/schema.rb
|
||||
files/*
|
||||
lib/redmine/scm/adapters/mercurial/redminehelper.pyc
|
||||
lib/redmine/scm/adapters/mercurial/redminehelper.pyo
|
||||
log/*.log*
|
||||
log/mongrel_debug
|
||||
public/dispatch.*
|
||||
public/plugin_assets
|
||||
tmp/*
|
||||
tmp/cache/*
|
||||
tmp/sessions/*
|
||||
tmp/sockets/*
|
||||
tmp/test/*
|
||||
vendor/rails
|
||||
*.rbc
|
||||
.svn/
|
||||
.git/
|
||||
59
plugins/redmine_workload/.rubocop.yml
Normal file
59
plugins/redmine_workload/.rubocop.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
DisplayCopNames: true
|
||||
DisplayStyleGuide: true
|
||||
TargetRubyVersion: 2.7
|
||||
|
||||
Exclude:
|
||||
- '**/vendor/**/*'
|
||||
- '**/tmp/**/*'
|
||||
- '**/bin/**/*'
|
||||
- '**/extra/**/*'
|
||||
- '**/lib/generators/**/templates/*'
|
||||
- '**/lib/tasks/**/*'
|
||||
- '**/files/**/*'
|
||||
- '**/test/**/*'
|
||||
- '**/db/migrate/*'
|
||||
- 'db/schema.rb'
|
||||
|
||||
require:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: true
|
||||
EnforcedStyle: always
|
||||
Exclude:
|
||||
- 'db/**/*.rb'
|
||||
- 'Gemfile'
|
||||
- 'Rakefile'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'test/**/*'
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/Encoding:
|
||||
Enabled: false
|
||||
|
||||
Layout/TrailingWhitespace:
|
||||
AllowInHeredoc: true
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Enabled: false
|
||||
|
||||
Rails/OutputSafety:
|
||||
Exclude:
|
||||
- 'app/helpers/workload_filters_helper.rb'
|
||||
- 'app/helpers/workloads_helper.rb'
|
||||
66
plugins/redmine_workload/.rubocop_todo.yml
Normal file
66
plugins/redmine_workload/.rubocop_todo.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2022-05-25 15:57:22 UTC using RuboCop version 1.28.2.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 10
|
||||
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 114
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
||||
# IgnoredMethods: refine
|
||||
Metrics/BlockLength:
|
||||
Max: 75
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountBlocks.
|
||||
Metrics/BlockNesting:
|
||||
Max: 4
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountComments, CountAsOne.
|
||||
Metrics/ClassLength:
|
||||
Max: 215
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 28
|
||||
|
||||
# Offense count: 15
|
||||
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 95
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 32
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: EnforcedStyle, AllowedPatterns, IgnoredPatterns.
|
||||
# SupportedStyles: snake_case, camelCase
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'app/controllers/workloads_controller.rb'
|
||||
- 'app/models/wl_national_holiday.rb'
|
||||
- 'app/models/wl_user_vacation.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||
# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to
|
||||
Naming/MethodParameterName:
|
||||
Exclude:
|
||||
- 'app/helpers/workload_filters_helper.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# Configuration parameters: EnforcedStyle, AllowedIdentifiers.
|
||||
# SupportedStyles: snake_case, camelCase
|
||||
Naming/VariableName:
|
||||
Exclude:
|
||||
- 'app/helpers/workload_filters_helper.rb'
|
||||
75
plugins/redmine_workload/CHANGELOG.md
Normal file
75
plugins/redmine_workload/CHANGELOG.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Changelog for Redmine Workload
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.2.1 - 2023-02-17
|
||||
|
||||
### Fixed
|
||||
|
||||
* nil error in data.keys.sort for very large time spans
|
||||
|
||||
## 2.2.0 - 2023-01-19
|
||||
|
||||
### Changed
|
||||
|
||||
* how to decide when an issue is overdue by comparing with a given date
|
||||
|
||||
## 2.1.0 - 2022-12-09
|
||||
|
||||
### Added
|
||||
|
||||
* Plugin setting 'workload_of_parent_issues' as option to include parent issues
|
||||
in the workload calculation
|
||||
|
||||
## 2.0.2 - 2022-11-14
|
||||
|
||||
### Added
|
||||
|
||||
* support for Redmine 5 with backward compatability to Redmine 4
|
||||
* translations for some permissions
|
||||
|
||||
### Fixed
|
||||
|
||||
* nil error in WorkloadsHelper#load_class_for_hour
|
||||
* nil error when user enters conflicting dates
|
||||
|
||||
## 2.0.1 - 2022-06-21
|
||||
|
||||
### Fixed
|
||||
|
||||
* undefined method 'id' in GroupWorkload#total_availabilities_of
|
||||
|
||||
## 2.0.0 - 2022-06-07
|
||||
|
||||
### Added
|
||||
|
||||
* week numbers to workload table header
|
||||
* group issues to workload table if a group is selected
|
||||
* calculation of group workload based on user main group setting
|
||||
* presentation of summarized group workload in workload table
|
||||
* additional infos about unscheduled issues
|
||||
* permissions :view_all_workloads, :view_own_group_workloads, :view_own_workloads
|
||||
* csv export of users or groups
|
||||
|
||||
### Changed
|
||||
|
||||
* using of dynamic action segments in routes due to deprecation warning
|
||||
* styling of workload table to look similar as gantt diagram
|
||||
* user and group selection to be in a separate class to make it reusable
|
||||
* permissions to be global again, i.e., not dependend of project module enabled
|
||||
* display of current user to show only if visited workload index page or when
|
||||
selected explicitly
|
||||
* error messages to translate field names
|
||||
|
||||
### Fixed
|
||||
|
||||
* broken unit test
|
||||
* missing closing selectors in some views causing the site footer to be displayed
|
||||
not at the bottom of the page
|
||||
|
||||
---
|
||||
|
||||
**NOTE** Changes prior and equal to version 1.1.0 are not reported.
|
||||
113
plugins/redmine_workload/README.md
Normal file
113
plugins/redmine_workload/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Workload Plugin for Redmine
|
||||
|
||||
A complete rewrite of the original workload-plugin from Rafael Calleja.
|
||||
The plugin calculates how much work each user would have to do per day in order to hit the deadlines for all his issues.
|
||||
It also calculates this information for a [group](https://www.redmine.org/projects/redmine/wiki/RedmineGroups).
|
||||
It calculates issues (number and hours) that are behind schedule and calculates issues that are unplanned (number and hours) so far.
|
||||
To be able to do all this calculations, the issues start date, due date and estimated time must be filled in.
|
||||
Issues that have not filled in one of these fields will be shown in the overview, but the workload resulting from these issues will be ignored.
|
||||
|
||||

|
||||
|
||||
## New Features in Version 2.2.0
|
||||
|
||||
### how to decide if an issue is overdue
|
||||
|
||||
Redefines the overdue state of an issue. Instead of comparing issue.due_date with the User.current.today (as in Issue#overdue?) it will compare by the
|
||||
date given by the user.
|
||||
|
||||
This change allows a workload analysis independent of the current date leading to more meaningful scenarios.
|
||||
|
||||
## New Features in Version 2.1.0
|
||||
|
||||
### consider workload of parent issues
|
||||
|
||||
By default parent issues are ignored when calculating workloads. With this setting the administrator can change the default behaviour by considering also all parent issues in the calculation.
|
||||
|
||||
## New Features in Version 2.0.x
|
||||
|
||||
Fortunately the German company [MENTOR GmbH & Co. Präzisions-Bauteile KG](https://www.mentor.de.com/) invested in this project to make these features possible:
|
||||
|
||||
### support of Redmine 5
|
||||
|
||||
Version 2.0.2 supports Redmine 5 and is backward compatible with Redmine 4.
|
||||
|
||||
### style-rework
|
||||
|
||||
The actual table has been a bit bulky and sticked out from the formatting of other areas. Especially when using themes like [Purplemine](https://github.com/mrliptontea/PurpleMine2).
|
||||
Now the css has been reworked to make the style more compact and gantt-like.
|
||||
|
||||
### workload per group
|
||||
|
||||
This introduces a new level of information for issues adressed to [groups](https://www.redmine.org/projects/redmine/wiki/RedmineGroups).
|
||||
It now can show informations about issues adressed to a group and calculates the workload of this group.
|
||||
To avoid missleading informations therefore each user needs to define the group where he/she puts his/her effort in.
|
||||
|
||||
### unplanned issues
|
||||
|
||||
The Plugin now calculates "unplanned" issues. This applys to issues that dont have a `start date` or a `due date`.
|
||||
The result now is shown close to overdue issues.
|
||||
|
||||
### export
|
||||
|
||||
The only way to have a look on the data was the workload page.
|
||||
There has been no way to transfer data, e.g. to excel, to draw some charts.
|
||||
Now there is a feature to export the workload per user and per role to build charts external.
|
||||
|
||||
## Installation / Uninstallation
|
||||
|
||||
Please refer to [redmine.org -> Plugins](https://www.redmine.org/projects/redmine/wiki/Plugins)
|
||||
|
||||
## How it Works
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
There are three places where this plugin might be configured:
|
||||
|
||||
1. In the plugin settings, available in the administration area under `plugins`.
|
||||
You can configure working days, thresholds here and set global holidays.
|
||||
|
||||
2. In the roles section of the administration area, the plugin adds new permissions as described below.
|
||||
There is no need to configure this plugin on project level.
|
||||
|
||||
3. On the workload page each user can setup his vacations.
|
||||
Here thresholds can be set divergent to global configuration. The `main group` needs to be set.
|
||||
|
||||
|
||||
## Permissions
|
||||
|
||||
The plugin shows the workload as follows:
|
||||
* An *admin user* can see the workload of everyone and configure user independent settings.
|
||||
* Any normal user can **see** workload as configured per role under project permissions:
|
||||
- *view own workloads*: a role with this permission can see own workload
|
||||
- *view own group workloads*: a role with this permission can see workloads of all users in his/her configured `main group`
|
||||
- *view all workloads*: a role with this permission can see workload of everyone.
|
||||
- When showing the issues that contribute to the workload, only issues visible to the current user are shown. Invisible issues are only summarized.
|
||||
* Any normal user can **configure** own settings as set per role under project permissions:
|
||||
- *Edit national Holidays*
|
||||
- *Edit own vacations*
|
||||
- *Edit own workload thresholds*
|
||||
|
||||
## Holidays, Vacation and User Workload Data
|
||||
|
||||
National holidays and user vacation is counted as day off (like weekend).
|
||||
Admins can setup National Holidays in plugin settings.
|
||||
Users can get permissions to setup their vacations and workload data with 'Roles and permissions'.
|
||||
You can specify user(s), who should be able to setup national holidays with 'Roles and permissions'.
|
||||
|
||||
## CSV-Export
|
||||
|
||||
Here you can export the values that are shown in the browser to use it in other systems (e.g. draw charts).
|
||||
|
||||
|Column|possible values|description|
|
||||
|------|---------------|-----------|
|
||||
|Status|planned, available|Describes if this Line shows planned workload or available hours per day.|
|
||||
|Type|aggregation, group, user|Describes if this Line shows hours for one `user`, hours that assigned to a `group` or hours that are `aggregated` for the group.|
|
||||
|Main group|*group-name*|Reports the configured `main group` for a `user` and (implicit) for a `group`. Is empty in case of aggregation.|
|
||||
|Number of overdue issues|*number*|Number of issues that are behind schedule.|
|
||||
|Hours of overdue issues|*hours*|Aggregated hours of issues that are behind schedule.|
|
||||
|Number of unplanned issues|*number*|Number of issues that are unplanned.|
|
||||
|Hours of unplanned issues|*hours*|Aggregated hours of issues that are unplanned.|
|
||||
|..date..|*datum* and *hours*|Column is named from the belonging datum. Lists per line the hours per day.|
|
||||
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
|
||||
class WlNationalHolidayController < ApplicationController
|
||||
include WlUserDataFinder
|
||||
|
||||
before_action :authorize_global, only: %i[create update destroy]
|
||||
before_action :find_user_workload_data
|
||||
before_action :select_year
|
||||
|
||||
helper :workloads
|
||||
|
||||
def index
|
||||
filter_year_start = Date.new(@this_year, 0o1, 0o1)
|
||||
filter_year_end = Date.new(@this_year, 12, 31)
|
||||
@wl_national_holiday = WlNationalHoliday.where('start between ? AND ?', filter_year_start, filter_year_end)
|
||||
@is_allowed = User.current.allowed_to_globally?(:edit_national_holiday)
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
@wl_national_holiday = begin
|
||||
WlNationalHoliday.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@wl_national_holiday = WlNationalHoliday.new(wl_national_holiday_params)
|
||||
if @wl_national_holiday.save
|
||||
flash[:notice] = l(:notice_holiday_saved)
|
||||
redirect_to action: 'index', year: params[:year]
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render :new
|
||||
end
|
||||
format.api { render_validation_errors(@wl_national_holiday) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@wl_national_holiday = begin
|
||||
WlNationalHoliday.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if @wl_national_holiday.update(wl_national_holiday_params)
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_holiday_updated)
|
||||
redirect_to(action: 'index', params: { year: params[:year] })
|
||||
end
|
||||
format.xml { head :ok }
|
||||
else
|
||||
format.html do
|
||||
render action: 'edit'
|
||||
end
|
||||
format.xml { render xml: @wl_national_holiday.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@wl_national_holiday = begin
|
||||
WlNationalHoliday.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
@wl_national_holiday.destroy
|
||||
flash[:notice] = l(:notice_holiday_deleted)
|
||||
redirect_to(action: 'index', year: params[:year])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_year
|
||||
if params[:year]
|
||||
@this_year = params[:year].to_i
|
||||
elsif @this_year.blank?
|
||||
@this_year = Time.zone.today.strftime('%Y').to_i
|
||||
end
|
||||
end
|
||||
|
||||
def wl_national_holiday_params
|
||||
params.require(:wl_national_holiday).permit(:start, :end, :reason)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WlUserDatasController < ApplicationController
|
||||
include WlUserDataFinder
|
||||
|
||||
helper :workloads
|
||||
helper :wl_user_datas
|
||||
|
||||
before_action :authorize_global, only: %i[update]
|
||||
before_action :find_user_workload_data, only: %i[edit update]
|
||||
|
||||
def edit
|
||||
@is_allowed = User.current.allowed_to_globally?(:edit_user_data)
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @user_workload_data.update(wl_user_data_params)
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_settings_updated)
|
||||
redirect_to workloads_path
|
||||
end
|
||||
format.xml { head :ok }
|
||||
else
|
||||
format.html do
|
||||
render :edit
|
||||
end
|
||||
format.xml { render xml: @user_workload_data.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wl_user_data_params
|
||||
params.require(:wl_user_data).permit(:user_id, :threshold_lowload_min, :threshold_normalload_min,
|
||||
:threshold_highload_min, :main_group)
|
||||
end
|
||||
end
|
||||
90
plugins/redmine_workload/app/controllers/wl_user_vacations_controller.rb
Executable file
90
plugins/redmine_workload/app/controllers/wl_user_vacations_controller.rb
Executable file
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WlUserVacationsController < ApplicationController
|
||||
include WlUserDataFinder
|
||||
|
||||
helper :workloads
|
||||
|
||||
before_action :authorize_global, only: %i[create update destroy]
|
||||
before_action :find_user_workload_data
|
||||
|
||||
def index
|
||||
@is_allowed = User.current.allowed_to_globally?(:edit_user_vacations)
|
||||
@wl_user_vacations = User.current.wl_user_vacations
|
||||
end
|
||||
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
@wl_user_vacation = begin
|
||||
WlUserVacation.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@wl_user_vacation = WlUserVacation.new(wl_user_vacations_params)
|
||||
@wl_user_vacation.user_id = User.current.id
|
||||
|
||||
respond_to do |format|
|
||||
if @wl_user_vacation.save
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_user_vacation_saved)
|
||||
redirect_to(action: 'index', params: { year: params[:year] })
|
||||
end
|
||||
else
|
||||
format.html do
|
||||
render action: 'new'
|
||||
end
|
||||
format.api { render_validation_errors(@wl_user_vacation) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@wl_user_vacation = begin
|
||||
WlUserVacation.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
respond_to do |format|
|
||||
if @wl_user_vacation.update(wl_user_vacation_params)
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_user_vacation_saved)
|
||||
redirect_to(action: 'index', params: { year: params[:year] })
|
||||
end
|
||||
else
|
||||
format.html do
|
||||
render action: 'edit'
|
||||
end
|
||||
format.xml { render xml: @wl_user_vacation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@wl_user_vacation = begin
|
||||
WlUserVacation.find(params[:id])
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
@wl_user_vacation.destroy
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:notice] = l(:notice_user_vacation_deleted)
|
||||
redirect_to(action: 'index', params: { year: params[:year] })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wl_user_vacations_params
|
||||
params.require(:wl_user_vacations).permit(:user_id, :date_from, :date_to, :comments, :vacation_type)
|
||||
end
|
||||
|
||||
def wl_user_vacation_params
|
||||
params.require(:wl_user_vacation).permit(:id, :user_id, :date_from, :date_to, :comments, :vacation_type)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WorkloadsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
helper :gantt
|
||||
helper :issues
|
||||
helper :projects
|
||||
helper :queries
|
||||
helper :workload_filters
|
||||
helper :workloads
|
||||
|
||||
include QueriesHelper
|
||||
include WlUserDataFinder
|
||||
include WorkloadsHelper
|
||||
|
||||
before_action :authorize_global, only: %i[index]
|
||||
before_action :find_user_workload_data
|
||||
|
||||
def index
|
||||
@first_day = sanitizeDateParameter(workload_params[:first_day], Time.zone.today - 10)
|
||||
@last_day = sanitizeDateParameter(workload_params[:last_day], Time.zone.today + 50)
|
||||
@today = sanitizeDateParameter(workload_params[:start_date], Time.zone.today)
|
||||
@date_check = @last_day >= @first_day
|
||||
|
||||
# if @today ("select as today") is before @first_day take @today as @first_day
|
||||
@first_day = [@today, @first_day].min
|
||||
|
||||
# Make sure that last_day is at most 12 months after first_day to prevent
|
||||
# long running times
|
||||
@last_day = [(@first_day >> 12) - 1, @last_day].min
|
||||
@time_span_to_display = @first_day..@last_day
|
||||
|
||||
if @date_check
|
||||
@groups = WlGroupSelection.new(groups: workload_params[:groups])
|
||||
@users = WlUserSelection.new(users: workload_params[:users], group_selection: @groups)
|
||||
|
||||
assignees = @users.all_selected
|
||||
user_workload = UserWorkload.new(assignees: assignees,
|
||||
time_span: @time_span_to_display,
|
||||
today: @today)
|
||||
|
||||
@months_to_render = WlDateTools.months_in_time_span(@time_span_to_display)
|
||||
@workload_data = user_workload.hours_per_user_issue_and_day
|
||||
|
||||
@group_workload = GroupWorkload.new(users: @users,
|
||||
user_workload: @workload_data,
|
||||
time_span: @time_span_to_display)
|
||||
|
||||
@workload = groups?(@groups) ? @group_workload : user_workload
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash.now[:error] = l(:error_date_setting) unless @date_check
|
||||
render action: :index
|
||||
end
|
||||
format.csv do
|
||||
send_data(workloads_to_csv(@workload, params), type: 'text/csv; header=present', filename: 'workload.csv')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Prepares workload params based on params[:workload] and params[:filter_type]
|
||||
# where the latter is relevant for exporting the data via csv.
|
||||
#
|
||||
def workload_params
|
||||
wl_params = params[:workload]&.merge(filter_type: params[:filter_type]) || {}
|
||||
return wl_params if wl_params[:filter_type].blank?
|
||||
|
||||
wl_params.merge(assignee_ids)
|
||||
end
|
||||
|
||||
def assignee_ids
|
||||
filter = params[:filter_type]&.first
|
||||
return if filter.blank?
|
||||
|
||||
groups = filter.include? 'groups'
|
||||
groups ? { groups: WlGroupSelection.new.all_group_ids } : { users: WlUserSelection.new.all_user_ids }
|
||||
end
|
||||
|
||||
def sanitizeDateParameter(parameter, default)
|
||||
if parameter.respond_to?(:to_date)
|
||||
parameter.to_date
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
end
|
||||
12
plugins/redmine_workload/app/helpers/wl_user_datas_helper.rb
Normal file
12
plugins/redmine_workload/app/helpers/wl_user_datas_helper.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Provides some helper methods for WlUserData related forms.
|
||||
#
|
||||
module WlUserDatasHelper
|
||||
def user_groups_for_select(selected:)
|
||||
options = []
|
||||
options += WlUserData.own_groups.pluck(:lastname, :id)
|
||||
options_for_select(options, selected)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WorkloadFiltersHelper
|
||||
def user_options_for_select(usersToShow, selectedUsers)
|
||||
result = ''
|
||||
return unless usersToShow
|
||||
|
||||
usersToShow.each do |user|
|
||||
selected = selectedUsers.include?(user) ? 'selected="selected"' : ''
|
||||
|
||||
result += "<option value=\"#{h(user.id)}\" #{selected}>#{h(user.name)}</option>"
|
||||
end
|
||||
|
||||
result.html_safe
|
||||
end
|
||||
|
||||
def group_options_for_select(groupsToShow, selectedGroups)
|
||||
result = ''
|
||||
return unless groupsToShow
|
||||
|
||||
groupsToShow.each do |group|
|
||||
selected = selectedGroups.include?(group) ? 'selected="selected"' : ''
|
||||
|
||||
result += "<option value=\"#{h(group&.id)}\" #{selected}>#{h(group.lastname)}</option>"
|
||||
end
|
||||
|
||||
result.html_safe
|
||||
end
|
||||
end
|
||||
124
plugins/redmine_workload/app/helpers/workloads_helper.rb
Normal file
124
plugins/redmine_workload/app/helpers/workloads_helper.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WorkloadsHelper
|
||||
def render_action_links
|
||||
render partial: 'wl_shared/action_links'
|
||||
end
|
||||
|
||||
##
|
||||
# Writes the css class for a group, user, and project combined.
|
||||
# @see css_group_class
|
||||
# @see css_user_class
|
||||
# @see css_project_class
|
||||
#
|
||||
def css_group_user_project_class(group_id, user_id, project_id)
|
||||
"#{css_group_class(group_id)} #{css_user_class(user_id)} #{css_project_class(project_id)}"
|
||||
end
|
||||
|
||||
##
|
||||
# Writes the css class for a project.
|
||||
# @param project_id [Integer] The project id.
|
||||
#
|
||||
def css_project_class(project_id)
|
||||
return unless project_id
|
||||
|
||||
"project-#{project_id}"
|
||||
end
|
||||
|
||||
##
|
||||
# Writes the css class for a group and user combined.
|
||||
# @see css_group_class
|
||||
# @see css_user_class
|
||||
#
|
||||
def css_group_user_class(group_id, user_id)
|
||||
"#{css_group_class(group_id)} #{css_user_class(user_id)}"
|
||||
end
|
||||
|
||||
##
|
||||
# Writes the css class for a group.
|
||||
# @param group_id [Integer] The group id.
|
||||
#
|
||||
def css_group_class(group_id)
|
||||
return unless group_id
|
||||
|
||||
"group-#{group_id}"
|
||||
end
|
||||
|
||||
##
|
||||
# Writes the css class for a user.
|
||||
# @param user_id [Integer] The user id.
|
||||
#
|
||||
def css_user_class(user_id)
|
||||
return unless user_id
|
||||
|
||||
"user-#{user_id}"
|
||||
end
|
||||
|
||||
##
|
||||
# Determines the css class for hours in dependence of the workload level.
|
||||
#
|
||||
# @param hours [Float] The decimal number of hours.
|
||||
# @param lowload [Float] The threshold lowload min value.
|
||||
# @param normalload [Float] The threshold normalload min value.
|
||||
# @param highload [Float] The threshold highload min value.
|
||||
# @return [String] The css class for highlighting the hours in the workload table.
|
||||
#
|
||||
def load_class_for_hours(hours, lowload, normalload, highload)
|
||||
hours = hours.to_f
|
||||
|
||||
if lowload && hours < lowload
|
||||
'none'
|
||||
elsif normalload && hours < normalload
|
||||
'low'
|
||||
elsif highload && hours < highload
|
||||
'normal'
|
||||
else
|
||||
'high'
|
||||
end
|
||||
end
|
||||
|
||||
def groups?(groups)
|
||||
return false unless groups
|
||||
|
||||
groups.selected.presence
|
||||
end
|
||||
|
||||
def filter_type(groups)
|
||||
groups?(groups) ? 'groups' : 'users'
|
||||
end
|
||||
|
||||
def workloads_to_csv(workload, params)
|
||||
prepare = WlCsvExporter.new(data: workload, params: params)
|
||||
Redmine::Export::CSV.generate(encoding: params[:encoding]) do |csv|
|
||||
csv << prepare.header_fields
|
||||
prepare.group_workload.each do |level, data|
|
||||
csv << prepare.line(level, data, :available) if level.instance_of? Group
|
||||
csv << prepare.line(level, data, :planned)
|
||||
end
|
||||
prepare.user_workload.each do |level, data|
|
||||
csv << prepare.line(level, data, :planned)
|
||||
end
|
||||
csv
|
||||
end
|
||||
end
|
||||
|
||||
def workload_params_as_hidden_field_tags(params)
|
||||
tags = ''
|
||||
params[:workload]&.each do |key, value|
|
||||
tags += if value.is_a? Array
|
||||
array_to_hidden_field(key, value)
|
||||
else
|
||||
hidden_field_tag("workload[#{key}]", value)
|
||||
end
|
||||
end
|
||||
tags.html_safe
|
||||
end
|
||||
|
||||
def array_to_hidden_field(key, value)
|
||||
tags = ''
|
||||
value.each do |entry|
|
||||
tags += hidden_field_tag("workload[#{key}][]", entry)
|
||||
end
|
||||
tags
|
||||
end
|
||||
end
|
||||
84
plugins/redmine_workload/app/models/group_user_dummy.rb
Normal file
84
plugins/redmine_workload/app/models/group_user_dummy.rb
Normal 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
|
||||
156
plugins/redmine_workload/app/models/group_workload.rb
Normal file
156
plugins/redmine_workload/app/models/group_workload.rb
Normal 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
|
||||
372
plugins/redmine_workload/app/models/user_workload.rb
Normal file
372
plugins/redmine_workload/app/models/user_workload.rb
Normal 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
|
||||
41
plugins/redmine_workload/app/models/wl_day_capacity.rb
Normal file
41
plugins/redmine_workload/app/models/wl_day_capacity.rb
Normal 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
|
||||
20
plugins/redmine_workload/app/models/wl_default_user_data.rb
Normal file
20
plugins/redmine_workload/app/models/wl_default_user_data.rb
Normal 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
|
||||
79
plugins/redmine_workload/app/models/wl_group_selection.rb
Normal file
79
plugins/redmine_workload/app/models/wl_group_selection.rb
Normal 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
|
||||
27
plugins/redmine_workload/app/models/wl_national_holiday.rb
Normal file
27
plugins/redmine_workload/app/models/wl_national_holiday.rb
Normal 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
|
||||
36
plugins/redmine_workload/app/models/wl_user_data.rb
Normal file
36
plugins/redmine_workload/app/models/wl_user_data.rb
Normal 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
|
||||
153
plugins/redmine_workload/app/models/wl_user_selection.rb
Normal file
153
plugins/redmine_workload/app/models/wl_user_selection.rb
Normal 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
|
||||
30
plugins/redmine_workload/app/models/wl_user_vacation.rb
Normal file
30
plugins/redmine_workload/app/models/wl_user_vacation.rb
Normal 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
|
||||
@@ -0,0 +1,145 @@
|
||||
<%
|
||||
# This file provides configuration options for the workload plugin.
|
||||
%>
|
||||
<fieldset class="box tabular">
|
||||
<legend><%= l(:workload_settings_general_workdays) %></legend>
|
||||
|
||||
<p><%= l(:workload_settings_general_workdays_explanation) %></p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_monday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_monday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_monday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_monday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_tuesday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_tuesday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_tuesday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_tuesday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_wednesday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_wednesday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_wednesday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_wednesday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_thursday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_thursday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_thursday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_thursday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_friday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_friday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_friday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_friday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_saturday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[general_workday_saturday]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[general_workday_saturday]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['general_workday_saturday'] != '' %>
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_general_workdays_sunday) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[workload_of_parent_issues]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[workload_of_parent_issues]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['workload_of_parent_issues'] != '' %>
|
||||
>
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset class="box tabular">
|
||||
<legend><%= l(:workload_settings_hours) %></legend>
|
||||
|
||||
<p>
|
||||
<%= l(:workload_settings_leastdailyworkload) %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_low_min) %></label>
|
||||
<input type="text"
|
||||
name="settings[threshold_lowload_min]"
|
||||
value="<%= settings['threshold_lowload_min'] %>"
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_normal_min) %></label>
|
||||
<input type="text"
|
||||
name="settings[threshold_normalload_min]"
|
||||
value="<%= settings['threshold_normalload_min'] %>"
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_high_min) %></label>
|
||||
<input type="text"
|
||||
name="settings[threshold_highload_min]"
|
||||
value="<%= settings['threshold_highload_min'] %>"
|
||||
>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="box">
|
||||
<legend><%=l(:workload_holiday_title) %></legend>
|
||||
|
||||
<%= link_to l(:workload_settings_holiday_setup), :controller => 'wl_national_holiday', :action => "index" %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='box'>
|
||||
<legend><%= l(:label_workload_calculation) %></legend>
|
||||
<p>
|
||||
<label><%= l(:label_include_parent_tasks) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[workload_of_parent_issues]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[workload_of_parent_issues]"
|
||||
value="checked"
|
||||
<%= "checked" if settings['workload_of_parent_issues'] != '' %>
|
||||
>
|
||||
<em class='info'><%= l(:info_include_parent_tasks) %></em>
|
||||
<em class='info icon icon-warning'><%= l(:warning_include_parent_tasks) %></em>
|
||||
</p>
|
||||
</fieldset>
|
||||
16
plugins/redmine_workload/app/views/wl_national_holiday/_form.html.erb
Executable file
16
plugins/redmine_workload/app/views/wl_national_holiday/_form.html.erb
Executable file
@@ -0,0 +1,16 @@
|
||||
<p>
|
||||
<label><%= l(:field_start_date) %>:</label><br>
|
||||
<%= form.date_select :start, default: { year: @this_year} %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_user_vacation_date_end) %>:</label><br>
|
||||
<%= form.date_select :end, default: { year: @this_year} %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:workload_holiday_reason) %>:</label><br>
|
||||
<%= form.text_field :reason %>
|
||||
</p>
|
||||
<input type="hidden"
|
||||
name="year"
|
||||
value="<%=@this_year%>">
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= translate 'id' %></th>
|
||||
<th><%= translate 'start' %></th>
|
||||
<th><%= translate 'end' %></th>
|
||||
<th><%= translate 'reason' %></th>
|
||||
<% if is_allowed %>
|
||||
<th colspan="2"></th>
|
||||
<% end %>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for holiday in wl_national_holiday -%>
|
||||
<tr class="<%= cycle("odd", "even") %>">
|
||||
<td><%= holiday.id.to_s %></td>
|
||||
<td><%= holiday.start.blank? ? '' : holiday.start.to_date.to_s %></td>
|
||||
<td><%= holiday.end.blank? ? '' : holiday.end.to_date.to_s %></td>
|
||||
<td><%= holiday.reason.blank? ? '' : holiday.reason %></td>
|
||||
<% if is_allowed %>
|
||||
<td><%= link_to l(:button_edit), edit_wl_national_holiday_path(holiday, :year => @this_year) %></td>
|
||||
<td><%= link_to l(:button_delete), wl_national_holiday_path(holiday, :year => @this_year),
|
||||
method: :delete,
|
||||
data: { confirm: l(:text_are_you_sure) } %></td>
|
||||
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end -%>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<%= error_messages_for 'wl_national_holiday' %>
|
||||
|
||||
<%= form_for @wl_national_holiday do |f| %>
|
||||
|
||||
<%= render(partial: "form", locals: {form: f}) %>
|
||||
<p>
|
||||
<%= f.submit l(:button_update)%>
|
||||
<%= link_to l(:button_cancel), :controller => 'wl_national_holiday', :action => "index", :year => @this_year %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="contextual">
|
||||
<%= render_action_links %>
|
||||
<%= call_hook(:view_wl_menu_extension) %>
|
||||
</div>
|
||||
|
||||
<h2><%= l(:workload_holiday_title)%></h2>
|
||||
|
||||
<%= link_to l(:label_new), {controller: "wl_national_holiday", action: "new"}, class: "icon icon-add" if @is_allowed%>
|
||||
|
||||
<p id="year-nav" >
|
||||
<%= link_to "<<", :controller => 'wl_national_holiday', :action => "index", :year => @this_year-1 %>
|
||||
<%= @this_year %>
|
||||
<%= link_to ">>", :controller => 'wl_national_holiday', :action => "index", :year => @this_year+1 %>
|
||||
</p>
|
||||
<% unless @wl_national_holiday.empty?%>
|
||||
|
||||
<%= render(partial: "show_list", locals: {wl_national_holiday: @wl_national_holiday, is_allowed: @is_allowed}) %>
|
||||
<%end%>
|
||||
|
||||
12
plugins/redmine_workload/app/views/wl_national_holiday/new.html.erb
Executable file
12
plugins/redmine_workload/app/views/wl_national_holiday/new.html.erb
Executable file
@@ -0,0 +1,12 @@
|
||||
<%= error_messages_for 'wl_national_holiday' %>
|
||||
|
||||
<h1><%= l(:workload_settings_holiday_new)%></h1>
|
||||
<%= form_for :wl_national_holiday, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
|
||||
|
||||
<%= render(partial: "form", locals: {form: f}) %>
|
||||
<p>
|
||||
<%= f.submit l(:button_add)%>
|
||||
<%= link_to l(:button_cancel), :controller => 'wl_national_holiday', :action => "index", :year => @this_year %>
|
||||
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,4 @@
|
||||
<%= link_to(l(:workload_title), controller: 'workloads', action: 'index') %>
|
||||
<%= link_to(l(:workload_holiday_title), controller: 'wl_national_holiday', action: 'index') %>
|
||||
<%= link_to(l(:workload_user_data_title), edit_wl_user_data_path(@user_workload_data)) %>
|
||||
<%= link_to(l(:workload_user_vacation_menu), controller: 'wl_user_vacations', action: 'index') %>
|
||||
@@ -0,0 +1 @@
|
||||
<% # Nothing yet %>
|
||||
@@ -0,0 +1,18 @@
|
||||
<p>
|
||||
<%
|
||||
label_symbol = "workload_settings_general_workdays_" + workday
|
||||
|
||||
# this is for testing only, needs to be initialized from model
|
||||
@user_working_days[ workday ] = ''
|
||||
%>
|
||||
<label><%= l(label_symbol.to_sym) %></label>
|
||||
<input type="hidden"
|
||||
name="settings[<%= workday %>]"
|
||||
value=""
|
||||
>
|
||||
<input type="checkbox"
|
||||
name="settings[<%= workday %>]"
|
||||
value="checked"
|
||||
<%= "checked" if @user_working_days[ workday ] != '' %>
|
||||
>
|
||||
</p>
|
||||
@@ -0,0 +1,57 @@
|
||||
<%= error_messages_for 'user_workload_data' %>
|
||||
|
||||
<div class="contextual">
|
||||
<%= render_action_links %>
|
||||
</div>
|
||||
|
||||
<% html_title(l(:workload_user_data_site_title)) %>
|
||||
|
||||
<h2><%= l(:workload_user_data_site_title) %> » <%= User.current.name %></h2>
|
||||
|
||||
<%= form_for @user_workload_data,
|
||||
url: wl_user_data_path,
|
||||
html: { :id => 'my_workload_user_data_form' } do |f| %>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
|
||||
<fieldset class="box tabular">
|
||||
<legend><%= l(:workload_settings_hours) %></legend>
|
||||
|
||||
<p>
|
||||
<%= l(:workload_settings_leastdailyworkload) %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_low_min) %></label>
|
||||
<%= f.text_field :threshold_lowload_min, disabled: !@is_allowed %>
|
||||
<%= l(:field_default_value)%>: <%= Setting['plugin_redmine_workload']['threshold_lowload_min']%>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_normal_min) %></label>
|
||||
<%= f.text_field :threshold_normalload_min, disabled: !@is_allowed %>
|
||||
<%= l(:field_default_value)%>: <%= Setting['plugin_redmine_workload']['threshold_normalload_min']%>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_hours_high_min) %></label>
|
||||
<%= f.text_field :threshold_highload_min, disabled: !@is_allowed %>
|
||||
<%= l(:field_default_value)%>: <%= Setting['plugin_redmine_workload']['threshold_highload_min']%>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_settings_main_group) %></label>
|
||||
<%= f.select :main_group,
|
||||
user_groups_for_select(selected: @user_workload_data.main_group),
|
||||
{ include_blank: true, disabled: !@is_allowed } %>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
<p class="mobile-hide">
|
||||
<% if @is_allowed %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
||||
<%= link_to l(:button_cancel), :back %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'sidebar' %>
|
||||
<% end %>
|
||||
21
plugins/redmine_workload/app/views/wl_user_vacations/_form.html.erb
Executable file
21
plugins/redmine_workload/app/views/wl_user_vacations/_form.html.erb
Executable file
@@ -0,0 +1,21 @@
|
||||
<p>
|
||||
<label><%= l(:field_start_date) %>:</label><br>
|
||||
<%= form.date_select :date_from, default: { year: @this_year} %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_user_vacation_date_end) %>:</label><br>
|
||||
<%= form.date_select :date_to, default: { year: @this_year} %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:workload_user_vacation_comments) %>:</label><br>
|
||||
<%= form.text_field :comments %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:workload_user_vacation_type) %>:</label><br>
|
||||
<%= form.text_field :vacation_type %>
|
||||
</p>
|
||||
|
||||
<input type="hidden"
|
||||
name="year"
|
||||
value="<%=@this_year%>">
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="autoscroll">
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= translate 'start' %></th>
|
||||
<th><%= translate 'end' %></th>
|
||||
<th><%= l(:workload_user_vacation_type) %></th>
|
||||
<th><%= l(:field_comments) %></th>
|
||||
<% if is_allowed %>
|
||||
<th colspan="2"></th>
|
||||
<% end %>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for vacation in wl_user_vacations -%>
|
||||
<tr class="<%= cycle("odd", "even") %>">
|
||||
<td><%= vacation.date_from.blank? ? '' : vacation.date_from.to_date.to_s %></td>
|
||||
<td><%= vacation.date_to.blank? ? '' : vacation.date_to.to_date.to_s %></td>
|
||||
<td><%= vacation.vacation_type.blank? ? '' : vacation.vacation_type %></td>
|
||||
<td><%= vacation.comments.blank? ? '' : vacation.comments %></td>
|
||||
<% if is_allowed %>
|
||||
<td><%= link_to l(:button_edit), edit_wl_user_vacation_path(vacation) %></td>
|
||||
<td><%= link_to l(:button_delete), wl_user_vacation_path(vacation),
|
||||
method: :delete,
|
||||
data: { confirm: l(:text_are_you_sure) } %></td>
|
||||
<% end %>
|
||||
|
||||
</tr>
|
||||
<% end -%>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%= error_messages_for 'wl_user_vacation' %>
|
||||
|
||||
<h2><%= l(:workload_user_vacation_edit)%></h2>
|
||||
<%= form_for @wl_user_vacation do |f| %>
|
||||
|
||||
<%= render(partial: "form", locals: {form: f}) %>
|
||||
<p>
|
||||
<%= f.submit l(:button_update)%>
|
||||
<%= link_to l(:button_cancel), :controller => 'wl_user_vacations', :action => "index", :year => @this_year %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="contextual">
|
||||
<%= render_action_links %>
|
||||
</div>
|
||||
|
||||
<h2><%= l(:workload_user_vacation_site_title) %> » <%= User.current.name %></h2>
|
||||
|
||||
<%= link_to l(:label_new), new_wl_user_vacation_path, :class => 'icon icon-add' if @is_allowed %>
|
||||
|
||||
<% unless @wl_user_vacations.empty?%>
|
||||
<%= render(partial: "show_list", locals: {wl_user_vacations: @wl_user_vacations, is_allowed: @is_allowed}) %>
|
||||
<%end%>
|
||||
@@ -0,0 +1,12 @@
|
||||
<%= error_messages_for 'wl_user_vacation' %>
|
||||
|
||||
<h2><%= l(:workload_user_vacation_new)%></h2>
|
||||
<%= form_for :wl_user_vacations, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
|
||||
|
||||
<%= render(partial: "form", locals: {form: f}) %>
|
||||
<p>
|
||||
<%= f.submit l(:button_add)%>
|
||||
<%= link_to l(:button_cancel), :controller => 'wl_user_vacations', :action => "index", :year => @this_year %>
|
||||
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<%
|
||||
# Parameters:
|
||||
# dayOfMonth: Any day of the month to render the header for.
|
||||
%>
|
||||
|
||||
<% @time_span_to_display&.each do |currentDay|%>
|
||||
<%
|
||||
if (currentDay.day == 1) then
|
||||
klass = ' firstDayOfMonth'
|
||||
elsif (currentDay.day == currentDay.end_of_month.day) then
|
||||
klass = ' lastDayOfMonth'
|
||||
else
|
||||
klass = ''
|
||||
end
|
||||
%>
|
||||
<th class="day-of-month<%= klass %>" scope="col">
|
||||
<%= tag.small currentDay.day.to_s %>
|
||||
</th>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<%
|
||||
# Parameters:
|
||||
# dayOfWeek: Any day of the week to render the header for.
|
||||
%>
|
||||
|
||||
<% @time_span_to_display&.each do |currentDay|%>
|
||||
<%
|
||||
if (currentDay.cwday == 1) then
|
||||
klass = ' firstDayOfWeek'
|
||||
elsif (currentDay.cwday == 7) then
|
||||
klass = ' lastDayOfWeek'
|
||||
else
|
||||
klass = ''
|
||||
end
|
||||
%>
|
||||
<th class="workload_hdr day-of-week <%= klass %>" scope="col">
|
||||
<%= tag.small day_name(currentDay.cwday).first %>
|
||||
</th>
|
||||
<% end %>
|
||||
23
plugins/redmine_workload/app/views/workloads/_export.erb
Normal file
23
plugins/redmine_workload/app/views/workloads/_export.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<% other_formats_links do |f| %>
|
||||
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
|
||||
<% end %>
|
||||
|
||||
<div id="csv-export-options" style="display: none;">
|
||||
<h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
|
||||
<%= form_tag(workloads_path(:format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
|
||||
<%= workload_params_as_hidden_field_tags(params) %>
|
||||
<p>
|
||||
<% if User.current.allowed_to_globally? :view_all_workloads %>
|
||||
<label><%= radio_button_tag 'filter_type[]', '', true %> <%= l("description_selected_#{filter_type(groups)}") %></label><br />
|
||||
<label><%= radio_button_tag 'filter_type[]', "all_#{filter_type(groups)}" %> <%= l("description_all_#{filter_type(groups)}") %></label>
|
||||
<% else %>
|
||||
<%= hidden_field_tag 'filter_type[]', '' %>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= export_csv_encoding_select_tag %>
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);", :data => { :disable_with => false }, id: 'csv-export-button' %>
|
||||
<%= link_to_function l(:button_cancel), "hideModal(this);" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
43
plugins/redmine_workload/app/views/workloads/_filters.erb
Normal file
43
plugins/redmine_workload/app/views/workloads/_filters.erb
Normal file
@@ -0,0 +1,43 @@
|
||||
<%= form_tag({}, {:method => :get, :id => 'filter_form', :class => 'filters'}) do %>
|
||||
|
||||
<h3><%=l(:workload_title)%> <%=l(:workload_show_filters) %></h3>
|
||||
<fieldset>
|
||||
<legend><%= l(:workload_show_range) %></legend>
|
||||
<div class="timespan">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<%= label_tag :workload_first_day, l(:workload_show_rangefrom) %>
|
||||
</td>
|
||||
<td>
|
||||
<%= text_field_tag :workload_first_day, @first_day, :name => 'workload[first_day]', :size => 10 %>
|
||||
<%= calendar_for('workload_first_day') %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= label_tag :workload_last_day, l(:workload_show_rangeto) %></td>
|
||||
<td><%= text_field_tag :workload_last_day, @last_day, :name => 'workload[last_day]', :size => 10 %><%= calendar_for('workload_last_day') %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= label_tag :workload_start_date, l(:workload_show_today) %></td>
|
||||
<td><%= text_field_tag :workload_start_date, @today, :name => 'workload[start_date]', :size => 10 %><%= calendar_for('workload_start_date') %></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
<br>
|
||||
<fieldset>
|
||||
<legend><%= l(:workload_show_filter_user_legend) %></legend>
|
||||
<div class="users">
|
||||
<%= label_tag :workload_users, l(:workload_show_filter_user) %>
|
||||
<%= select_tag :workload_users, user_options_for_select(@users&.allowed_to_display, @users&.selected), :name => 'workload[users][]', :multiple => true, :onchange => "this.form.workload_groups.selectedIndex=-1;" %>
|
||||
</div>
|
||||
<br>
|
||||
<div class="groups">
|
||||
<%= label_tag :workload_groups, l(:workload_show_filter_group) %>
|
||||
<%= select_tag :workload_groups, group_options_for_select(@groups&.allowed_to_display, @groups&.selected), :name => 'workload[groups][]', :multiple => true, :onchange => "this.form.workload_users.selectedIndex=-1;" %>
|
||||
</div>
|
||||
<%= link_to_function l(:button_apply), 'jQuery("#filter_form").submit()', :class => 'apply icon icon-checked' %>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%
|
||||
# Renders the headers of the month names
|
||||
%>
|
||||
|
||||
<% @months_to_render&.each do |month| %>
|
||||
<th class="workload_hdr" colspan="<%= (month[:last_day].day - month[:first_day].day) + 1 %>" scope="colgroup">
|
||||
<% if ((month[:last_day] - month[:first_day]) + 1) >= 5 then %>
|
||||
<%= tag.small "#{month_name(month[:first_day].month)} #{month[:first_day].year}" %>
|
||||
<% end %>
|
||||
</th>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<%
|
||||
# Parameters:
|
||||
# num_of_week: Any week of the year to render the header for.
|
||||
%>
|
||||
|
||||
<% @time_span_to_display&.each do |current_day|%>
|
||||
<%
|
||||
if (current_day.cwday == 1) then
|
||||
klass = ' first-day-of-week'
|
||||
elsif (current_day.cwday == 7) then
|
||||
klass = ' last-day-of-week'
|
||||
else
|
||||
klass = ''
|
||||
end
|
||||
%>
|
||||
<th class="num-of-week<%= klass %>" scope="col">
|
||||
<%= tag.small current_day.cweek if current_day.cwday == 4 %>
|
||||
</th>
|
||||
<% end %>
|
||||
@@ -0,0 +1,27 @@
|
||||
<%
|
||||
# Renders the workload data for one single issue.
|
||||
# Parameters:
|
||||
# * assignee: The user or group to render the data for.
|
||||
# * summarizedWorkload Hash that contains the summarized workload for invisible issues.
|
||||
%>
|
||||
|
||||
<% summarizedWorkload.keys.sort.each do |day| %>
|
||||
<%
|
||||
hours = summarizedWorkload[day][:hours]
|
||||
holiday = summarizedWorkload[day][:holiday]
|
||||
lowload = summarizedWorkload[day][:lowload]
|
||||
normalload = summarizedWorkload[day][:normalload]
|
||||
highload = summarizedWorkload[day][:highload]
|
||||
klass = 'hours'
|
||||
klass += ' holiday' if holiday
|
||||
klass += ' today' if @today === day
|
||||
klass += ' ' + load_class_for_hours(hours, lowload, normalload, highload)
|
||||
|
||||
hoursString = (hours.abs < 0.01) ? '' : sprintf("%.1f", hours, assignee)
|
||||
%>
|
||||
<td class="<%= klass %>">
|
||||
<span>
|
||||
<%= hoursString %>
|
||||
</span>
|
||||
</td>
|
||||
<% end %>
|
||||
@@ -0,0 +1,26 @@
|
||||
<%
|
||||
# Renders an accumulated workload.
|
||||
# Parameters:
|
||||
# * totalWorkload: Hash that contains the total workload for each day.
|
||||
%>
|
||||
|
||||
<% totalWorkload.keys.sort.each do |day| %>
|
||||
<%
|
||||
hours = totalWorkload[day][:hours]
|
||||
holiday = totalWorkload[day][:holiday]
|
||||
lowload = totalWorkload[day][:lowload]
|
||||
normalload = totalWorkload[day][:normalload]
|
||||
highload = totalWorkload[day][:highload]
|
||||
|
||||
klass = 'hours'
|
||||
klass += ' holiday' if holiday
|
||||
klass += ' workingday' if !holiday
|
||||
klass += ' today' if @today === day
|
||||
klass += ' ' + load_class_for_hours(hours, lowload, normalload, highload)
|
||||
%>
|
||||
<td class="<%= klass %>">
|
||||
<span>
|
||||
<%= sprintf("%.1f", hours) %>
|
||||
</span>
|
||||
</td>
|
||||
<% end %>
|
||||
@@ -0,0 +1,9 @@
|
||||
<%#
|
||||
# Creates a trigger to open and close parts of the workload view.
|
||||
# Parameters:
|
||||
# trigger_for: set as "data-for"-attribute
|
||||
#
|
||||
# ▶ is a right-pointing filled triangle.
|
||||
%>
|
||||
|
||||
<span class="trigger closed" data-for="<%= trigger_for %>">▶</span>
|
||||
@@ -0,0 +1,43 @@
|
||||
<%
|
||||
# Renders the workload data for a single group.
|
||||
# Parameters:
|
||||
# * group: The group to render the data for.
|
||||
# * data: The data to render. A hash with issues as keys.
|
||||
%>
|
||||
|
||||
<tbody class="group-total-workload" id="group-total-workload-<%= group&.id %>">
|
||||
<tr>
|
||||
<th class="group-description <%= css_group_class(group&.id) %>" scope="row" title="<%= l(:workload_trigger_tooltip) %>">
|
||||
<%= render :partial => 'trigger', :locals => {:trigger_for => css_group_class(group&.id)} %>
|
||||
<%= "#{group.firstname} #{group.lastname}" %>
|
||||
<% if data[:overdue_number]&.positive? || data[:unscheduled_number]&.positive? %>
|
||||
<dl class="additional-group-info">
|
||||
<dt><%= l(:workload_overdue_issues_num) %></dt>
|
||||
<dd><%= data[:overdue_number] %></dd>
|
||||
<dt><%= l(:workload_overdue_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:overdue_hours] %></dd>
|
||||
<dt class='mt-5'><%= l(:workload_unscheduled_issues_num) %></dt>
|
||||
<dd class='mt-5'><%= data[:unscheduled_number] %></dd>
|
||||
<dt><%= l(:workload_unscheduled_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:unscheduled_hours] %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</th>
|
||||
<% # Print the total workload for this group for each day %>
|
||||
<% user = GroupUserDummy.new(group: group) %>
|
||||
<%= render :partial => 'total_workload', :locals => {:totalWorkload => data[:total], :user => user } %>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% if data[:invisible].presence %>
|
||||
<tbody class="invisible-issues-summary <%= css_group_class(group&.id) %>">
|
||||
<tr>
|
||||
<th class="invisible-workload-description" scope="row"><%= l(:workload_show_invisible_issues) %> </th>
|
||||
<%= render :partial => 'summarized_workload_for_invisible_issues', :locals => {:assignee => group, :summarizedWorkload => data[:invisible]} %>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end %>
|
||||
<% # Iterate over all assignees for the group %>
|
||||
<% assignees = data.keys.select{|key| key.kind_of?(User) || key.kind_of?(Group) || key.kind_of?(GroupUserDummy) } %>
|
||||
<% assignees.each do |assignee| %>
|
||||
<%= render :partial => 'workload_for_user_in_group', :locals => {:group => group, :user => assignee, :data => data[assignee]} %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,47 @@
|
||||
<%
|
||||
# Renders the workload data for one single issue.
|
||||
# Parameters:
|
||||
# * user: The user to render the data for.
|
||||
# * issue: The issue to render the data for.
|
||||
# * data: The data to render. A hash with days as keys.
|
||||
# * index: Index of the issue for this user.
|
||||
%>
|
||||
|
||||
<%
|
||||
klass = (index % 2 == 0) ? 'even' : 'odd'
|
||||
klass += ' overdue' if !issue.due_date.nil? && (issue.due_date < @today)
|
||||
%>
|
||||
|
||||
<tr class="issue-workloads <%= css_group_user_project_class(group&.id, user&.id, issue.project&.id) %> <%= klass %>">
|
||||
<th class="issue-description" scope="row">
|
||||
<div class="tooltip"><%= link_to_issue(issue) %>
|
||||
<span class="tip"><%= render_issue_tooltip(issue) %></span>
|
||||
</div>
|
||||
</th>
|
||||
<%
|
||||
|
||||
data.keys.compact.sort.each do |day|
|
||||
dataForDay = data[day]
|
||||
hours = dataForDay[:hours]
|
||||
lowload = dataForDay[:lowload]
|
||||
normalload = dataForDay[:normalload]
|
||||
highload = dataForDay[:highload]
|
||||
klass = 'hours'
|
||||
klass += ' active' if dataForDay[:active]
|
||||
klass += ' not-active' if !dataForDay[:active]
|
||||
klass += ' holiday' if dataForDay[:holiday]
|
||||
klass += ' workingday' if !dataForDay[:holiday]
|
||||
klass += ' not-estimated' if dataForDay[:noEstimate]
|
||||
klass += ' estimated' if !dataForDay[:noEstimate]
|
||||
klass += ' today' if @today === day
|
||||
klass += ' ' + load_class_for_hours(hours, lowload, normalload, highload)
|
||||
|
||||
hoursString = (hours.abs < 0.01) ? '' : sprintf("%.1f", hours)
|
||||
%>
|
||||
<td class="<%= klass %>">
|
||||
<span>
|
||||
<%= hoursString %>
|
||||
</span>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
@@ -0,0 +1,38 @@
|
||||
<%#
|
||||
# Renders the workload data for one single project for one single user.
|
||||
# Parameters:
|
||||
# * user: The user to render the data for.
|
||||
# * project: The project to render
|
||||
# * data: The data to render. A hash with issues as keys.
|
||||
%>
|
||||
|
||||
<tbody class="project-total-workload <%= css_group_user_class(group&.id, user&.id) %>">
|
||||
<tr>
|
||||
<th class="project-description <%= css_group_user_project_class(group&.id, user&.id, project&.id) %>" scope="row" title="<%= l(:workload_trigger_tooltip) %>">
|
||||
<%= render :partial => 'trigger', :locals => {:trigger_for => css_group_user_project_class(group&.id, user&.id, project&.id) } %>
|
||||
<%= project.to_s %>
|
||||
<% if data[:overdue_number]&.positive? || data[:unscheduled_number]&.positive? %>
|
||||
<dl class="additional-project-info">
|
||||
<dt><%= l(:workload_overdue_issues_num) %></dt>
|
||||
<dd><%= data[:overdue_number] %></dd>
|
||||
<dt><%= l(:workload_overdue_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:overdue_hours] %></dd>
|
||||
<dt class='mt-5'><%= l(:workload_unscheduled_issues_num) %></dt>
|
||||
<dd class='mt-5'><%= data[:unscheduled_number] %></dd>
|
||||
<dt><%= l(:workload_unscheduled_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:unscheduled_hours] %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</th>
|
||||
<% # Print the total workload for this project for each day %>
|
||||
<%= render :partial => 'total_workload', :locals => { :totalWorkload => data[:total], :user => user } %>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody class="issue-workloads <%= css_group_user_project_class(group&.id, user&.id, project&.id) %>">
|
||||
<% # Iterate over all issues for the project %>
|
||||
<% issuesForUser = data.keys.select{|x| x.kind_of?(Issue)} %>
|
||||
<% issuesForUser.each_with_index do |issue, index| %>
|
||||
|
||||
<%= render :partial => 'workload_for_issue', :locals => { :group => group, :user => user, :issue => issue, :data => data[issue], :index => index } %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
@@ -0,0 +1,43 @@
|
||||
<%
|
||||
# Renders the workload data for one single user.
|
||||
# Parameters:
|
||||
# * user: The user to render the data for.
|
||||
# * data: The data to render. A hash with issues as keys.
|
||||
%>
|
||||
|
||||
<tbody class="user-total-workload" id="user-total-workload-<%= user.id %>">
|
||||
<tr>
|
||||
<th class="user-description <%= css_user_class(user&.id) %>" scope="row" title="<%= l(:workload_trigger_tooltip) %>">
|
||||
<%= render :partial => 'trigger', :locals => { :trigger_for => css_user_class(user&.id) } %>
|
||||
<%= "#{user.firstname} #{user.lastname}" %>
|
||||
<% if data[:overdue_number]&.positive? || data[:unscheduled_number]&.positive? %>
|
||||
<dl class="additional-user-info">
|
||||
<dt><%= l(:workload_overdue_issues_num) %></dt>
|
||||
<dd><%= data[:overdue_number] %></dd>
|
||||
<dt><%= l(:workload_overdue_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:overdue_hours] %></dd>
|
||||
<dt class='mt-5'><%= l(:workload_unscheduled_issues_num) %></dt>
|
||||
<dd class='mt-5'><%= data[:unscheduled_number] %></dd>
|
||||
<dt><%= l(:workload_unscheduled_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:unscheduled_hours] %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</th>
|
||||
<% # Print the total workload for this user for each day %>
|
||||
<%= render :partial => 'total_workload', :locals => { :totalWorkload => data[:total], :user => user } %>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<% if data[:invisible].presence %>
|
||||
<tbody class="invisible-issues-summary <%= css_user_class(user&.id) %>">
|
||||
<tr>
|
||||
<th class="invisible-workload-description" scope="row"><%= l(:workload_show_invisible_issues) %> </th>
|
||||
<%= render :partial => 'summarized_workload_for_invisible_issues', :locals => { :assignee => user, :summarizedWorkload => data[:invisible] } %>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end %>
|
||||
<% # Iterate over all projects for the user %>
|
||||
<% projects = data.keys.select{|x| x.kind_of?(Project)} %>
|
||||
<% projects.each do |project| %>
|
||||
<%= render :partial => 'workload_for_project', :locals => { :group => nil, :user => user, :project => project, :data => data[project] } %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,45 @@
|
||||
<%
|
||||
# Renders the workload data for one single user.
|
||||
# Parameters:
|
||||
# * user: The user to render the data for.
|
||||
# * data: The data to render. A hash with issues as keys.
|
||||
%>
|
||||
|
||||
<tbody class="user-total-workload-in-<%= css_group_class(group&.id) %> user-total-workload <%= css_user_class(user&.id) %>">
|
||||
<tr>
|
||||
<th class="user-description <%= css_group_class(group&.id) %> <%= css_user_class(user&.id) %>" scope="row" title="<%= l(:workload_trigger_tooltip) %>">
|
||||
<%= render :partial => 'trigger', :locals => { :trigger_for => css_group_user_class(group&.id, user&.id) } %>
|
||||
<%= "#{user.firstname} #{user.lastname}" %>
|
||||
<% if data[:overdue_number]&.positive? || data[:unscheduled_number]&.positive? %>
|
||||
<dl class="additional-user-info">
|
||||
<dt><%= l(:workload_overdue_issues_num) %></dt>
|
||||
<dd><%= data[:overdue_number] %></dd>
|
||||
<dt><%= l(:workload_overdue_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:overdue_hours] %></dd>
|
||||
<dt class='mt-5'><%= l(:workload_unscheduled_issues_num) %></dt>
|
||||
<dd class='mt-5'><%= data[:unscheduled_number] %></dd>
|
||||
<dt><%= l(:workload_unscheduled_issues_hours) %></dt>
|
||||
<dd><%= "%0.2f" % data[:unscheduled_hours] %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</th>
|
||||
<% # Print the total workload for this user for each day %>
|
||||
<% if data[:total].presence %>
|
||||
<%= render :partial => 'total_workload', :locals => {:totalWorkload => data[:total], :user => user} %>
|
||||
<% end %>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<% if data[:invisible].presence %>
|
||||
<tbody class="invisible-issues-summary <%= css_group_class(group&.id) %>">
|
||||
<tr>
|
||||
<th class="invisible-workload-description" scope="row"><%= l(:workload_show_invisible_issues) %> </th>
|
||||
<%= render :partial => 'summarized_workload_for_invisible_issues', :locals => {:assignee => user, :summarizedWorkload => data[:invisible]} %>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end %>
|
||||
<% # Iterate over all projects for the user %>
|
||||
<% projects = data.keys.select{|x| x.kind_of?(Project)} %>
|
||||
<% projects.each do |project| %>
|
||||
<%= render :partial => 'workload_for_project', :locals => {:user => user, :group => group, :project => project, :data => data[project]} %>
|
||||
<% end %>
|
||||
51
plugins/redmine_workload/app/views/workloads/index.html.erb
Normal file
51
plugins/redmine_workload/app/views/workloads/index.html.erb
Normal file
@@ -0,0 +1,51 @@
|
||||
<% html_title(l(:workload_site_title)) %>
|
||||
|
||||
<div class="contextual">
|
||||
<%= render_action_links %>
|
||||
</div>
|
||||
|
||||
<h2><%= l(:workload_show_label) %></h2>
|
||||
|
||||
<% if @date_check %>
|
||||
<%= error_messages_for 'query' %>
|
||||
<div class="wrapper">
|
||||
<table class="data">
|
||||
<thead>
|
||||
<tr class="workload_hdr">
|
||||
<td class="workload_hdr" rowspan="4"> <!-- empty space --></td>
|
||||
<%= render :partial => 'month_names_header' %>
|
||||
</tr>
|
||||
<tr class="workload_hdr" >
|
||||
<%= render :partial => 'num_of_week_header' %>
|
||||
</tr>
|
||||
<tr class="workload_hdr">
|
||||
<%= render :partial => 'day_of_month_header' %>
|
||||
</tr>
|
||||
<tr class="workload_hdr">
|
||||
<%= render :partial => 'day_of_week_header' %>
|
||||
</tr>
|
||||
</thead>
|
||||
<% if groups? @groups %>
|
||||
<% @group_workload&.by_group&.each do |group, data| %>
|
||||
<%= render :partial => 'workload_for_group',
|
||||
:locals => { :group => group,
|
||||
:data => data }
|
||||
%>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% @workload_data&.keys&.each do |user| %>
|
||||
<%= render :partial => 'workload_for_user',
|
||||
:locals => { :group => nil,
|
||||
:user => user,
|
||||
:data => @workload_data[user] }
|
||||
%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'export', locals: { groups: @groups } %>
|
||||
<% end %>
|
||||
<% content_for :sidebar do %>
|
||||
<%= render partial: 'filters' %>
|
||||
<% end %>
|
||||
BIN
plugins/redmine_workload/assets/images/background-holiday.png
Normal file
BIN
plugins/redmine_workload/assets/images/background-holiday.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 B |
BIN
plugins/redmine_workload/assets/images/background-holiday.xcf
Normal file
BIN
plugins/redmine_workload/assets/images/background-holiday.xcf
Normal file
Binary file not shown.
BIN
plugins/redmine_workload/assets/images/logo.png
Normal file
BIN
plugins/redmine_workload/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
plugins/redmine_workload/assets/images/logo.xcf
Normal file
BIN
plugins/redmine_workload/assets/images/logo.xcf
Normal file
Binary file not shown.
64
plugins/redmine_workload/assets/javascripts/slides.js
Normal file
64
plugins/redmine_workload/assets/javascripts/slides.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
Toggle along the hierarchie tree.
|
||||
When opening, open level by level. When closing, close the item with all
|
||||
lower levels at once.
|
||||
*/
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.trigger').click(function() {
|
||||
var OPENED = '▼'
|
||||
var CLOSED = '▶'
|
||||
$(this).toggleClass('closed opened');
|
||||
|
||||
identifier = $(this).attr('data-for');
|
||||
identifierClasses = identifier.trim().replace(/\s/g, ".");
|
||||
|
||||
// topDownHierarchieChain shows current hierarchie level on the left and the css
|
||||
// class of the next hierarchie level on the right hand side.
|
||||
topDownHierarchieChain = new Map([
|
||||
["group-description " + identifier, ".user-total-workload-in-" + identifierClasses],
|
||||
["user-description " + identifier, ".project-total-workload." + identifierClasses],
|
||||
["project-description " + identifier, ".issue-workloads." + identifierClasses]
|
||||
]);
|
||||
|
||||
// bottomUpHierarchies shows current hierarchie level on the left and all
|
||||
// lower hierarchie levels on the right hand side.
|
||||
bottomUpHierarchieChain = new Map([
|
||||
["group-description " + identifier, [".issue-workloads." + identifierClasses,
|
||||
".project-total-workload." + identifierClasses,
|
||||
".user-total-workload-in-" + identifierClasses]],
|
||||
["user-description " + identifier, [".issue-workloads." + identifierClasses,
|
||||
".project-total-workload." + identifierClasses]],
|
||||
["project-description " + identifier, [".issue-workloads." + identifierClasses]]
|
||||
]);
|
||||
|
||||
currentHierarchieLevel = $(this).parent().attr('class');
|
||||
|
||||
if ($(this).hasClass('opened')) {
|
||||
$(this).show();
|
||||
// Shows additional info
|
||||
$(this).siblings().show();
|
||||
// Reveals the next hierarchie level
|
||||
nextHierarchieLevelClass = topDownHierarchieChain.get(currentHierarchieLevel);
|
||||
$(nextHierarchieLevelClass).each(function(){
|
||||
$(this).show(); // but keep its 'children' closed if any
|
||||
$(this).siblings('.invisible-issues-summary.' + identifierClasses).show();
|
||||
});
|
||||
$(this).html(OPENED);
|
||||
}
|
||||
else {
|
||||
lowerHierarchieLevelClasses = bottomUpHierarchieChain.get(currentHierarchieLevel);
|
||||
// Collapses all lower levels of the currentHierarchieLevel at once
|
||||
// as defined in bottomUpHierarchieChain.
|
||||
lowerHierarchieLevelClasses.forEach(function(css){
|
||||
$(css).hide();
|
||||
$(css).siblings('.invisible-issues-summary.' + identifierClasses).hide();
|
||||
currentHierarchieLevel = $(css).find('span.trigger.opened');
|
||||
currentHierarchieLevel.html(CLOSED);
|
||||
currentHierarchieLevel.siblings('dl').hide();
|
||||
})
|
||||
$(this).siblings().hide();
|
||||
$(this).html(CLOSED);
|
||||
}
|
||||
});
|
||||
});
|
||||
336
plugins/redmine_workload/assets/stylesheets/style.css
Normal file
336
plugins/redmine_workload/assets/stylesheets/style.css
Normal file
@@ -0,0 +1,336 @@
|
||||
/*******************************************************************************
|
||||
* Color scheme.
|
||||
******************************************************************************/
|
||||
:root {
|
||||
--lightgray: #eee;
|
||||
--mediumgray: #c0c0c0;
|
||||
--anthracite: #333;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Normalisation.
|
||||
******************************************************************************/
|
||||
|
||||
.controller-workloads table th, table td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.controller-workloads table dt {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.controller-workloads table dd {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Styles for the filter form.
|
||||
******************************************************************************/
|
||||
|
||||
.controller-workloads .filters > div,
|
||||
.controller-workloads .filters .apply {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.controller-workloads .filters .apply {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
legend {
|
||||
color: var(--anthracite);
|
||||
}
|
||||
|
||||
.controller-workloads .wrapper {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.controller-workloads .users select,
|
||||
.controller-workloads .groups select {
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
}
|
||||
/*******************************************************************************
|
||||
* Styles for the header of workload table.
|
||||
******************************************************************************/
|
||||
|
||||
.controller-workloads .data .workload_hdr {
|
||||
background-color: var(--lightgray);
|
||||
border: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
/* Column width if no data is displayed */
|
||||
.controller-workloads .data .day-of-month,
|
||||
.controller-workloads .data .day-of-week {
|
||||
min-width: 25px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* Month names */
|
||||
.controller-workloads .data .month-name {
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
border-top: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .month-name:last-child {
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
/* Num of week */
|
||||
.controller-workloads .data .num-of-week.first-day-of-week {
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .num-of-week.last-day-of-week {
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
|
||||
/* Day of month */
|
||||
.controller-workloads .data .day-of-month.firstDayOfMonth,
|
||||
.controller-workloads .data .day-of-month:first-child {
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .day-of-month:last-child {
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
/* Day of week */
|
||||
.controller-workloads .data .day-of-week.firstDayOfWeek,
|
||||
.controller-workloads .data .day-of-week:first-child {
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .day-of-week:last-child {
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Styles for the workload table.
|
||||
******************************************************************************/
|
||||
|
||||
.controller-workloads .wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.controller-workloads .data {
|
||||
table-layout: fixed;
|
||||
border-spacing: 0 3px;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.controller-workloads .data th {
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
* Styles for the stuff that may be shown or hidden.
|
||||
-----------------------------------------------------------------------------*/
|
||||
.controller-workloads .data .additional-group-info,
|
||||
.controller-workloads .data .additional-user-info,
|
||||
.controller-workloads .data .additional-project-info,
|
||||
.controller-workloads .data .invisible-issues-summary,
|
||||
.controller-workloads .data tbody[class^="user-total-workload-in-group-"],
|
||||
.controller-workloads .data .project-total-workload,
|
||||
.controller-workloads .data .issue-workloads {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controller-workloads .data .trigger {
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.controller-workloads table dt.mt-5,
|
||||
.controller-workloads table dd.mt-5 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
* Table heads on the left side.
|
||||
-----------------------------------------------------------------------------*/
|
||||
.controller-workloads .data .group-description,
|
||||
.controller-workloads .data .user-description,
|
||||
.controller-workloads .data .project-description,
|
||||
.controller-workloads .data .issue-description,
|
||||
.controller-workloads .data .invisible-workload-description {
|
||||
text-align: left;
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .group-description {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .user-description {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .project-description {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-description,
|
||||
.controller-workloads .data .invisible-workload-description {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .invisible-workload-description {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.controller-workloads .data .additional-group-info,
|
||||
.controller-workloads .data .additional-user-info,
|
||||
.controller-workloads .data .additional-project-info {
|
||||
font-size: 0.9em;
|
||||
font-weight: normal;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .additional-group-info dt,
|
||||
.controller-workloads .data .additional-user-info dt,
|
||||
.controller-workloads .data .additional-project-info dt {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .additional-group-info dd,
|
||||
.controller-workloads .data .additional-user-info dd,
|
||||
.controller-workloads .data .additional-project-info dd {
|
||||
margin-left: 210px;
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
* Real table data
|
||||
-----------------------------------------------------------------------------*/
|
||||
.controller-workloads .tooltip span.tip {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.controller-workloads td.hours {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.controller-workloads .data tr td:last-child {
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data tr:last-child td,
|
||||
.controller-workloads .data tr:last-child th {
|
||||
border-bottom: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .user-total-workload {
|
||||
background-color: #F6F7F8;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-workloads.odd {
|
||||
background-color: #F6F7F8;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-workloads.odd.overdue {
|
||||
background-color: #ecb4b4;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-workloads.even {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-workloads.even.overdue {
|
||||
background-color: #f8d0d0;
|
||||
}
|
||||
|
||||
.controller-workloads .data .issue-workloads:hover,
|
||||
.controller-workloads .data .group-total-workload:hover,
|
||||
.controller-workloads .data .user-total-workload:hover,
|
||||
.controller-workloads .data .project-total-workload:hover{
|
||||
background-color: #D7D7D7!important;
|
||||
}
|
||||
|
||||
.controller-workloads .data td {
|
||||
padding: 0;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
width: 25px;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.controller-workloads .data .today {
|
||||
border-right: 2px dashed red;
|
||||
padding-left: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* Styling of the spans in the table */
|
||||
.controller-workloads .data .hours span {
|
||||
display: block;
|
||||
padding-top: .6em;
|
||||
padding-bottom: .3em;
|
||||
height: 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.controller-workloads .data .hours.none span {
|
||||
color: var(--anthracite);
|
||||
}
|
||||
|
||||
.controller-workloads .data .none span {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.controller-workloads .data .active span {
|
||||
background-color: rgba(200, 200, 200, 0.5);
|
||||
}
|
||||
|
||||
.controller-workloads .data .low span {
|
||||
background: green;
|
||||
}
|
||||
|
||||
.controller-workloads .data .normal span {
|
||||
background: yellow;
|
||||
color: var(--anthracite);
|
||||
}
|
||||
|
||||
.controller-workloads .data .high span {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.controller-workloads .data .holiday {
|
||||
background-color: var(--lightgray);
|
||||
border-left: 1px solid var(--mediumgray);
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .holiday.today {
|
||||
background-color: var(--lightgray);
|
||||
border-left: 2px solid red;
|
||||
border-right: 1px solid var(--mediumgray);
|
||||
}
|
||||
|
||||
.controller-workloads .data .holiday span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controller-workloads .data .active.holiday span {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Styles for national holiday page.
|
||||
******************************************************************************/
|
||||
|
||||
#year-nav {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
106
plugins/redmine_workload/config/locales/de.yml
Executable file
106
plugins/redmine_workload/config/locales/de.yml
Executable file
@@ -0,0 +1,106 @@
|
||||
# German strings go here
|
||||
de:
|
||||
permission_edit_national_holiday: "Feiertage bearbeiten"
|
||||
permission_edit_user_vacations: "Eigene Urlaube bearbeiten"
|
||||
permission_edit_user_data: "Eigene Belastungsgrenzen bearbeiten"
|
||||
permission_view_all_workloads: "Alle Workloads anzeigen"
|
||||
permission_view_own_workloads: "Eigene Workloads anzeigen"
|
||||
permission_view_own_group_workloads: "Workloads eigener Gruppen anzeigen"
|
||||
workload_title: "Workload"
|
||||
workload_site_title: "Workload"
|
||||
workload_show_label: "Workload"
|
||||
workload_show_filters: "Filter"
|
||||
workload_show_range: "Anzeigebereich"
|
||||
workload_show_rangefrom: "von"
|
||||
workload_show_rangeto: "bis"
|
||||
workload_show_today: "Als \"Heute\" annehmen:"
|
||||
workload_show_filter_user_legend: "Nutzer- oder Gruppenfilter"
|
||||
workload_show_filter_user: "Wähle Nutzer"
|
||||
workload_show_filter_group: "Wähle Gruppe(n)"
|
||||
workload_show_issues: "Zeige Tickets:"
|
||||
workload_show_invisible_issues: "Für Sie unsichtbare Tickets:"
|
||||
workload_show_issue_estimated_hours: "Geschätzer Aufwand"
|
||||
workload_show_issue_spent_time: "Aufgewendete Zeit"
|
||||
workload_show_issue_status: "Status"
|
||||
workload_show_issue_percent_done: "% erledigt"
|
||||
workload_show_issue_percent_estimated: "% geschätzt"
|
||||
workload_show_dcr: "Resourceneinsatz Differenz:"
|
||||
workload_show_date: "Timing"
|
||||
workload_show_a_hours: "Effizienz"
|
||||
workload_show_user_total_hours_remaining: "Verbleibend"
|
||||
workload_show_issue_priority: "Priorität"
|
||||
workload_show_issue_date: "Start/Ende"
|
||||
workload_show_legend: "Legende"
|
||||
workload_show_legend_title: "Timing"
|
||||
workload_show_legend_perfect: "Perfekt"
|
||||
workload_show_legend_normal: "Pünktlich"
|
||||
workload_show_legend_retard: "Verzögerung"
|
||||
workload_show_legend_retard2: "Verzögerung"
|
||||
workload_show_legend_no_time: "Kein Timing"
|
||||
workload_show_legend_father: "Übergeordnetes Ticket (wird nicht mit aufsummiert)"
|
||||
workload_show_legend_time_spent: "Aufgewendete Zeit"
|
||||
workload_show_legend_past: "In der Vergangenheit"
|
||||
workload_show_legend_out: "Budget verbraucht"
|
||||
workload_trigger_tooltip: "Pfeil anklicken für mehr Details"
|
||||
workload_overdue_issues_num: "Anzahl überfälliger Tickets:"
|
||||
workload_overdue_issues_hours: "Stunden aus überfälligen Tickets:"
|
||||
workload_settings_general_workdays: "Wöchentliche Arbeitstage"
|
||||
workload_settings_general_workdays_explanation: "Diese Tage werden bei der Berechnung der Arbeitsbelastung als Arbeitstage angenommen:"
|
||||
workload_settings_general_workdays_monday: "Montag"
|
||||
workload_settings_general_workdays_tuesday: "Dienstag"
|
||||
workload_settings_general_workdays_wednesday: "Mittwoch"
|
||||
workload_settings_general_workdays_thursday: "Donnerstag"
|
||||
workload_settings_general_workdays_friday: "Freitag"
|
||||
workload_settings_general_workdays_saturday: "Samstag"
|
||||
workload_settings_general_workdays_sunday: "Sonntag"
|
||||
workload_settings_hours: "Belastungsgrenzen"
|
||||
workload_settings_leastdailyworkload: "Kleinste Arbeitsbelastung pro Tag, deren Last als ..."
|
||||
workload_settings_hours_low_min: "... niedrig angesehen wird:"
|
||||
workload_settings_hours_normal_min: "... normal angesehen wird:"
|
||||
workload_settings_hours_high_min: "... überlastet angesehen wird:"
|
||||
workload_settings_main_group: "Hauptgruppe"
|
||||
workload_settings_holiday_setup: "Feiertage bearbeiten"
|
||||
workload_settings_holiday_new: "Feiertag anlegen"
|
||||
workload_user_vacation_site_title: "Urlaubsübersicht"
|
||||
workload_user_vacation: "Urlaub"
|
||||
workload_user_vacation_menu: "Meine Urlaube"
|
||||
workload_user_vacation_new: "Neuen Urlaub anlegen"
|
||||
workload_user_vacation_edit: "Urlaub bearbeiten"
|
||||
workload_user_vacation_type: "Typ"
|
||||
workload_user_vacation_comments: "Kommentar"
|
||||
workload_user_vacation_date_end: "Ende"
|
||||
notice_user_vacation_saved: "Urlaub wurde erfolgreich gespeichert"
|
||||
notice_user_vacation_deleted: "Urlaub wurde erfolgreich gelöscht"
|
||||
workload_user_data_site_title: "Benutzer Workload Information"
|
||||
workload_user_data_title: "Meine Einstellungen"
|
||||
workload_holiday_title: "Feiertage"
|
||||
workload_holiday_reason: "Anlass"
|
||||
notice_settings_updated: Einstellungen erfolgreich aktualisiert.
|
||||
notice_holiday_updated: "Feiertag wurde erfolgreich aktualisiert"
|
||||
notice_holiday_saved: "Feiertag wurde erfolgreich gespeichert"
|
||||
notice_holiday_deleted: "Feiertag wurde erfolgreich gelöscht"
|
||||
label_assigned_to_group: Zugewiesen an %{value}
|
||||
description_selected_groups: Ausgewählte Gruppen
|
||||
description_all_groups: Alle Gruppen
|
||||
description_selected_users: Ausgewählte Nutzer
|
||||
description_all_users: Alle Nutzer
|
||||
field_main_group: Hauptgruppe
|
||||
field_number_of_overdue_issues: Anzahl überfälliger Tickets
|
||||
field_number_of_overdue_hours: Stunden aus überfälligen Tickets
|
||||
field_number_of_unscheduled_issues: Anzahl nicht geplanter Tickets
|
||||
field_number_of_unscheduled_hours: Stunden aus nicht geplanten Tickets
|
||||
label_planned: geplant
|
||||
label_available: verfügbar
|
||||
label_aggregation: Aggregation
|
||||
field_end: Ende
|
||||
field_start: Beginn
|
||||
field_date_to: Ende
|
||||
field_date_from: Beginn
|
||||
field_reason: Anlass
|
||||
workload_unscheduled_issues_num: 'Anzahl ungeplanter Tickets:'
|
||||
workload_unscheduled_issues_hours: 'Stunden aus ungeplanten Tickets:'
|
||||
error_date_setting: 'Überprüfen Sie Ihre Eingabe! Das Enddatum (bis) liegt vor dem Startdatum (von).'
|
||||
label_workload_calculation: Workloadberechnung
|
||||
label_include_parent_tasks: Hauptaufgaben einbeziehen
|
||||
info_include_parent_tasks: Werden Hauptaufgaben nicht einbezogen, wird lediglich der geschätzte Aufwand von Unteraufgaben berücksichtigt!
|
||||
warning_include_parent_tasks: Wenn der Workload von Haupt- und Unteraufgaben unabhängig voneinander berechnet werden soll, muss in Administration » Tickets » Eigenschaften übergeordneter Tickets das Feld %-erledigt angepasst werden.
|
||||
106
plugins/redmine_workload/config/locales/en.yml
Executable file
106
plugins/redmine_workload/config/locales/en.yml
Executable file
@@ -0,0 +1,106 @@
|
||||
# English strings go here
|
||||
en:
|
||||
permission_edit_national_holiday: "Edit national Holidays"
|
||||
permission_edit_user_vacations: "Edit own vacations"
|
||||
permission_edit_user_data: "Edit own workload thresholds"
|
||||
permission_view_all_workloads: "View all workloads"
|
||||
permission_view_own_workloads: "View own workloads"
|
||||
permission_view_own_group_workloads: "View own group workloads"
|
||||
workload_title: "Workload"
|
||||
workload_site_title: "Workload"
|
||||
workload_show_label: "Workload"
|
||||
workload_show_filters: "Filters"
|
||||
workload_show_range: "Range"
|
||||
workload_show_rangefrom: "from"
|
||||
workload_show_rangeto: "until"
|
||||
workload_show_today: "Use as \"today\":"
|
||||
workload_show_filter_user_legend: "Filter User or Groups"
|
||||
workload_show_filter_user: "Select User(s)"
|
||||
workload_show_filter_group: "Select Group(s)"
|
||||
workload_show_issues: "Show issues:"
|
||||
workload_show_invisible_issues: "Issues invisible to you:"
|
||||
workload_show_issue_estimated_hours: "Estimated"
|
||||
workload_show_issue_spent_time: "Spent time"
|
||||
workload_show_issue_status: "Status"
|
||||
workload_show_issue_percent_done: "% done"
|
||||
workload_show_issue_percent_estimated: "% estim."
|
||||
workload_show_dcr: "Resource consumption diff:"
|
||||
workload_show_date: "Timing"
|
||||
workload_show_a_hours: "Efficiency"
|
||||
workload_show_user_total_hours_remaining: "Remaining"
|
||||
workload_show_issue_priority: "Priority"
|
||||
workload_show_issue_date: "Start/End"
|
||||
workload_show_legend: "Legend"
|
||||
workload_show_legend_title: "Timing"
|
||||
workload_show_legend_perfect: "Perfect"
|
||||
workload_show_legend_normal: "On time"
|
||||
workload_show_legend_retard: "Delay"
|
||||
workload_show_legend_retard2: "Delay"
|
||||
workload_show_legend_no_time: "No timing"
|
||||
workload_show_legend_father: "Parent task (does not add up)"
|
||||
workload_show_legend_time_spent: "Time spent"
|
||||
workload_show_legend_past: "Past"
|
||||
workload_show_legend_out: "Out of budget"
|
||||
workload_trigger_tooltip: "Click the triangle for more details"
|
||||
workload_overdue_issues_num: "Number of overdue issues:"
|
||||
workload_overdue_issues_hours: "Hours from overdue issues:"
|
||||
workload_settings_general_workdays: "Weekly working days"
|
||||
workload_settings_general_workdays_explanation: "The following days will be considered as working days when computing the workload:"
|
||||
workload_settings_general_workdays_monday: "Monday:"
|
||||
workload_settings_general_workdays_tuesday: "Tuesday:"
|
||||
workload_settings_general_workdays_wednesday: "Wednesday:"
|
||||
workload_settings_general_workdays_thursday: "Thursday:"
|
||||
workload_settings_general_workdays_friday: "Friday:"
|
||||
workload_settings_general_workdays_saturday: "Saturday:"
|
||||
workload_settings_general_workdays_sunday: "Sunday:"
|
||||
workload_settings_hours: "Workload thresholds"
|
||||
workload_settings_leastdailyworkload: "Smallest workload that is considered to be ..."
|
||||
workload_settings_hours_low_min: "... low:"
|
||||
workload_settings_hours_normal_min: "... normal:"
|
||||
workload_settings_hours_high_min: "... overloaded:"
|
||||
workload_settings_main_group: "Main group"
|
||||
workload_settings_holiday_setup: "Edit holidays"
|
||||
workload_settings_holiday_new: "Create holiday"
|
||||
workload_user_vacation_site_title: "Vacation Overview"
|
||||
workload_user_vacation: "Vacation"
|
||||
workload_user_vacation_menu: "My Vacations"
|
||||
workload_user_vacation_new: "Create new Vacation"
|
||||
workload_user_vacation_edit: "Edit Vacation"
|
||||
workload_user_vacation_type: "Type"
|
||||
workload_user_vacation_comments: "Coment"
|
||||
workload_user_vacation_date_end: "End"
|
||||
notice_user_vacation_saved: "Vacation successfully saved"
|
||||
notice_user_vacation_deleted: "Vacation successfully deleted"
|
||||
workload_user_data_site_title: "User Workload Information"
|
||||
workload_user_data_title: "My Setup"
|
||||
workload_holiday_title: "Holidays"
|
||||
workload_holiday_reason: "Reason"
|
||||
notice_settings_updated: Settings sucessfully updated
|
||||
notice_holiday_updated: Holiday was successfully updated
|
||||
notice_holiday_saved: Holiday was successfully saved
|
||||
notice_holiday_deleted: Holiday was successfully deleted
|
||||
label_assigned_to_group: Assigned to %{value}
|
||||
description_selected_groups: Selected groups
|
||||
description_all_groups: All groups
|
||||
description_selected_users: Selected users
|
||||
description_all_users: All users
|
||||
field_main_group: Main group
|
||||
field_number_of_overdue_issues: Number of overdue issues
|
||||
field_number_of_overdue_hours: Hours of overdue issues
|
||||
field_number_of_unscheduled_issues: Number of unscheduled issues
|
||||
field_number_of_unscheduled_hours: Hours of unscheduled issues
|
||||
label_planned: planned
|
||||
label_available: available
|
||||
label_aggregation: Aggregation
|
||||
field_end: End
|
||||
field_start: Begin
|
||||
field_date_to: End
|
||||
field_date_from: Begin
|
||||
field_reason: Reason
|
||||
workload_unscheduled_issues_num: 'Number of unscheduled issues:'
|
||||
workload_unscheduled_issues_hours: 'Hours of unscheduled issues:'
|
||||
error_date_setting: 'Please check your data! The end date is before the start date.'
|
||||
label_workload_calculation: Workload calculation
|
||||
label_include_parent_tasks: Include parent tasks
|
||||
info_include_parent_tasks: If parent tasks would not be considered then only estimated times of child issues will be included in the workload calculation!
|
||||
warning_include_parent_tasks: If the workload of parent and child issues should be considered independently then you need to adjust the setting in Administration » Settings » Issue tracking » Parent tasks attributes accordingly.
|
||||
39
plugins/redmine_workload/config/locales/es.yml
Normal file
39
plugins/redmine_workload/config/locales/es.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# Spanish strings go here
|
||||
es:
|
||||
permission_edit_national_holiday: "Edit national Holidays"
|
||||
permission_edit_user_vacations: "Edit own vacations"
|
||||
permission_edit_user_data: "Edit own workload thresholds"
|
||||
permission_view_all_workloads: "View all workloads"
|
||||
permission_view_own_workloads: "View own workloads"
|
||||
permission_view_own_group_workloads: "View own group workloads"
|
||||
workload_title: "Workload"
|
||||
workload_site_title: "Workload"
|
||||
workload_show_label: "Workload"
|
||||
workload_show_filters: "Filters"
|
||||
workload_show_range: "Rango"
|
||||
workload_show_today: "Fecha de cálculo"
|
||||
workload_show_filter_user: "Usuarios"
|
||||
workload_show_issues: "Mostrar Tareas"
|
||||
workload_show_invisible_issues: "Issues invisible to you:"
|
||||
workload_show_issue_estimated_hours: "Estimated"
|
||||
workload_show_issue_spent_time: "Spent time"
|
||||
workload_show_issue_status: "Status"
|
||||
workload_show_issue_percent_done: "% done"
|
||||
workload_show_issue_percent_estimated: "% estim."
|
||||
workload_show_dcr: "Resource consumption diff:"
|
||||
workload_show_date: "Timing"
|
||||
workload_show_a_hours: "Efficiency"
|
||||
workload_show_user_total_hours_remaining: "Remaining"
|
||||
workload_show_issue_priority: "Priority"
|
||||
workload_show_issue_date: "Start/End"
|
||||
workload_show_legend: "Legend"
|
||||
workload_show_legend_title: "Timing"
|
||||
workload_show_legend_perfect: "Perfect"
|
||||
workload_show_legend_normal: "On time"
|
||||
workload_show_legend_retard: "Delay"
|
||||
workload_show_legend_retard2: "Delay"
|
||||
workload_show_legend_no_time: "No timing"
|
||||
workload_show_legend_father: "Parent task (does not add up)"
|
||||
workload_show_legend_time_spent: "Time spent"
|
||||
workload_show_legend_past: "Past"
|
||||
workload_show_legend_out: "Out of budget"
|
||||
39
plugins/redmine_workload/config/locales/fr.yml
Normal file
39
plugins/redmine_workload/config/locales/fr.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# French strings go here
|
||||
fr:
|
||||
permission_edit_national_holiday: "Edit national Holidays"
|
||||
permission_edit_user_vacations: "Edit own vacations"
|
||||
permission_edit_user_data: "Edit own workload thresholds"
|
||||
permission_view_all_workloads: "View all workloads"
|
||||
permission_view_own_workloads: "View own workloads"
|
||||
permission_view_own_group_workloads: "View own group workloads"
|
||||
workload_title: "Charge de Travail"
|
||||
workload_site_title: "Charge"
|
||||
workload_show_label: "Charge"
|
||||
workload_show_filters: "Filters"
|
||||
workload_show_range: "Écart"
|
||||
workload_show_today: "Aujourd'hui"
|
||||
workload_show_filter_user: "Utilisateurs"
|
||||
workload_show_issues: "Voir les tickets"
|
||||
workload_show_invisible_issues: "Issues invisible to you:"
|
||||
workload_show_issue_estimated_hours: "Estimé"
|
||||
workload_show_issue_spent_time: "Temps consumé"
|
||||
workload_show_issue_status: "État"
|
||||
workload_show_issue_percent_done: "% réalisé"
|
||||
workload_show_issue_percent_estimated: "% estim."
|
||||
workload_show_dcr: "Différence des ressources consommées"
|
||||
workload_show_date: "Timing"
|
||||
workload_show_a_hours: "Efficaticté"
|
||||
workload_show_user_total_hours_remaining: "Restant"
|
||||
workload_show_issue_priority: "Priorité"
|
||||
workload_show_issue_date: "DÉbut / Fin"
|
||||
workload_show_legend: "Légende"
|
||||
workload_show_legend_title: "Timing"
|
||||
workload_show_legend_perfect: "Parfait"
|
||||
workload_show_legend_normal: "Dans les temps"
|
||||
workload_show_legend_retard: "Retard"
|
||||
workload_show_legend_retard2: "Retartd important"
|
||||
workload_show_legend_no_time: "Non planifié"
|
||||
workload_show_legend_father: "tâche parent (non renseignée)"
|
||||
workload_show_legend_time_spent: "Temps passé"
|
||||
workload_show_legend_past: "Passé"
|
||||
workload_show_legend_out: "Hors budget"
|
||||
60
plugins/redmine_workload/config/locales/it.yml
Normal file
60
plugins/redmine_workload/config/locales/it.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
# Italian strings go here
|
||||
it:
|
||||
permission_edit_national_holiday: "Edit national Holidays"
|
||||
permission_edit_user_vacations: "Edit own vacations"
|
||||
permission_edit_user_data: "Edit own workload thresholds"
|
||||
permission_view_all_workloads: "View all workloads"
|
||||
permission_view_own_workloads: "View own workloads"
|
||||
permission_view_own_group_workloads: "View own group workloads"
|
||||
workload_title: "Workload"
|
||||
workload_site_title: "Workload"
|
||||
workload_show_label: "Workload"
|
||||
workload_show_filters: "Filtri"
|
||||
workload_show_range: "Periodo:"
|
||||
workload_show_rangefrom: "da:"
|
||||
workload_show_rangeto: "a:"
|
||||
workload_show_today: "Usa come \"OGGI\":"
|
||||
workload_show_filter_user: "Utenti:"
|
||||
workload_show_issues: "Mostra segnalazioni:"
|
||||
workload_show_invisible_issues: "Segnalazioni non visibili a te:"
|
||||
workload_show_issue_estimated_hours: "Stimato"
|
||||
workload_show_issue_spent_time: "Tempo trascorso"
|
||||
workload_show_issue_status: "Stato"
|
||||
workload_show_issue_percent_done: "% eseguito"
|
||||
workload_show_issue_percent_estimated: "% stimato."
|
||||
workload_show_dcr: "Differenza nell'impiego delle risorse:"
|
||||
workload_show_date: "Tempo"
|
||||
workload_show_a_hours: "Efficienza"
|
||||
workload_show_user_total_hours_remaining: "Rimanente"
|
||||
workload_show_issue_priority: "Priorità"
|
||||
workload_show_issue_date: "Start/End"
|
||||
workload_show_legend: "Legenda"
|
||||
workload_show_legend_title: "Tempo"
|
||||
workload_show_legend_perfect: "Perfetto"
|
||||
workload_show_legend_normal: "In tempo"
|
||||
workload_show_legend_retard: "Ritardo"
|
||||
workload_show_legend_retard2: "Ritardo2"
|
||||
workload_show_legend_no_time: "Senza tempo"
|
||||
workload_show_legend_father: "Parent task (does not add up)"
|
||||
workload_show_legend_time_spent: "Tempo impiegato"
|
||||
workload_show_legend_past: "Passato"
|
||||
workload_show_legend_out: "Fuori budget"
|
||||
workload_trigger_tooltip: "Click sul triangolo per maggiori dettagli"
|
||||
workload_overdue_issues_num: "Numero di segnalazioni scadute:"
|
||||
workload_overdue_issues_hours: "Ore da segnalazioni scadute:"
|
||||
|
||||
workload_settings_general_workdays: "Giorni lavorativi settimanali"
|
||||
workload_settings_general_workdays_explanation: "I seguenti giorni settimanali saranno considerati come lavorativi:"
|
||||
workload_settings_general_workdays_monday: "Lunedì:"
|
||||
workload_settings_general_workdays_tuesday: "Martedì:"
|
||||
workload_settings_general_workdays_wednesday: "Mercoledì:"
|
||||
workload_settings_general_workdays_thursday: "Giovedì:"
|
||||
workload_settings_general_workdays_friday: "Venerdì:"
|
||||
workload_settings_general_workdays_saturday: "Sabato:"
|
||||
workload_settings_general_workdays_sunday: "Domenica:"
|
||||
|
||||
workload_settings_hours: "Soglie di carico"
|
||||
workload_settings_leastdailyworkload: "La soglia minima che dev'essere considerata di carico ..."
|
||||
workload_settings_hours_low_min: "... basso:"
|
||||
workload_settings_hours_normal_min: "... normale:"
|
||||
workload_settings_hours_high_min: "... sovraccarico:"
|
||||
7
plugins/redmine_workload/config/routes.rb
Normal file
7
plugins/redmine_workload/config/routes.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
resources :workloads, only: %w[index]
|
||||
resources :wl_user_datas, only: %w[edit update]
|
||||
|
||||
resources :wl_national_holiday
|
||||
resources :wl_user_vacations
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateWlUserVacations < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :wl_user_vacations do |t|
|
||||
t.belongs_to :user, index: true, null: false
|
||||
t.column :date_from, :date, null: false
|
||||
t.column :date_to, :date, null: false
|
||||
t.column :comments, :string, limit: 255
|
||||
t.column :vacation_type, :string, limit: 255
|
||||
t.column :ref_id, :integer # optional: for sync purpose with external system
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateWlUserData < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :wl_user_datas do |t|
|
||||
t.belongs_to :user, index: true, null: false
|
||||
t.float :threshold_lowload_min, null: false
|
||||
t.float :threshold_normalload_min, null: false
|
||||
t.float :threshold_highload_min, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateWlNationalHolidays < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :wl_national_holidays do |t|
|
||||
t.date :start, null: false
|
||||
t.date :end, null: false
|
||||
t.string :reason, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Adds main_group column to store the users group which should be considered when
|
||||
# calculating group workloads.
|
||||
#
|
||||
class AddMainGroupToWlUserData < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :wl_user_datas, :main_group, :integer
|
||||
add_index :wl_user_datas, :main_group
|
||||
end
|
||||
end
|
||||
59
plugins/redmine_workload/init.rb
Executable file
59
plugins/redmine_workload/init.rb
Executable file
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'redmine'
|
||||
require File.expand_path('lib/redmine_workload', __dir__)
|
||||
|
||||
Redmine::Plugin.register :redmine_workload do
|
||||
name 'Redmine workload plugin'
|
||||
author 'Jost Baron, Liane Hampe, xmera Solutions GmbH'
|
||||
description 'This is a plugin for Redmine, originally developed by Rafael Calleja. It ' \
|
||||
'displays the estimated number of hours users and groups have to work to finish ' \
|
||||
'all their assigned issus on time.'
|
||||
version '2.2.1'
|
||||
url 'https://github.com/xmera-circle/redmine_workload'
|
||||
|
||||
menu :top_menu,
|
||||
:WorkLoad,
|
||||
{ controller: 'workloads', action: 'index' },
|
||||
caption: :workload_title,
|
||||
if: proc {
|
||||
User.current.logged? && User.current.allowed_to?({ controller: :workloads, action: :index },
|
||||
nil, global: true)
|
||||
}
|
||||
|
||||
settings partial: 'settings/workload_settings',
|
||||
default: {
|
||||
'general_workday_monday' => 'checked',
|
||||
'general_workday_tuesday' => 'checked',
|
||||
'general_workday_wednesday' => 'checked',
|
||||
'general_workday_thursday' => 'checked',
|
||||
'general_workday_friday' => 'checked',
|
||||
'general_workday_saturday' => '',
|
||||
'general_workday_sunday' => '',
|
||||
'threshold_lowload_min' => 0.1,
|
||||
'threshold_normalload_min' => 7,
|
||||
'threshold_highload_min' => 8.5,
|
||||
'workload_of_parent_issues' => ''
|
||||
}
|
||||
|
||||
permission :view_all_workloads, workloads: :index
|
||||
permission :view_own_workloads, workloads: :index
|
||||
permission :view_own_group_workloads, workloads: :index
|
||||
permission :edit_national_holiday, wl_national_holiday: %i[create update destroy]
|
||||
permission :edit_user_vacations, wl_user_vacations: %i[create update destroy]
|
||||
permission :edit_user_data, wl_user_datas: :update
|
||||
end
|
||||
|
||||
if Rails.version < '6'
|
||||
plugin = Redmine::Plugin.find(:redmine_workload)
|
||||
Rails.application.configure do
|
||||
config.autoload_paths << "#{plugin.directory}/app/presenters"
|
||||
end
|
||||
end
|
||||
|
||||
class RedmineToolbarHookListener < Redmine::Hook::ViewListener
|
||||
def view_layouts_base_html_head(_context)
|
||||
javascript_include_tag('slides', plugin: :redmine_workload) +
|
||||
stylesheet_link_tag('style', plugin: :redmine_workload)
|
||||
end
|
||||
end
|
||||
13
plugins/redmine_workload/lib/redmine_workload.rb
Normal file
13
plugins/redmine_workload/lib/redmine_workload.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'redmine_workload/extensions/user_patch'
|
||||
require_relative 'redmine_workload/hooks/plugin'
|
||||
require_relative 'redmine_workload/group_workload_preparer'
|
||||
require_relative 'redmine_workload/user_workload_preparer'
|
||||
require_relative 'redmine_workload/wl_calculation_restrictions'
|
||||
require_relative 'redmine_workload/wl_csv_exporter'
|
||||
require_relative 'redmine_workload/wl_date_tools'
|
||||
require_relative 'redmine_workload/wl_issue_query'
|
||||
require_relative 'redmine_workload/wl_issue_state'
|
||||
require_relative 'redmine_workload/wl_user_data_finder'
|
||||
require_relative 'redmine_workload/wl_user_data_defaults'
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RedmineWorkload
|
||||
module Extensions
|
||||
module UserPatch
|
||||
def self.prepended(base)
|
||||
base.prepend(InstanceMethods)
|
||||
base.class_eval do
|
||||
has_one :wl_user_data, inverse_of: :user
|
||||
has_many :wl_user_vacations, inverse_of: :user
|
||||
delegate :main_group, to: :wl_user_data, allow_nil: true
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
##
|
||||
# Prefer to use main_group_id over User#wl_user_data.main_group since
|
||||
# the latter may lead to
|
||||
# NoMethodError Exception: undefined method `main_group' for nil:NilClass
|
||||
# when no data set for wl_user_data exists. In contrast, the delegation
|
||||
# of main_group, as used below, will handle this case.
|
||||
#
|
||||
def main_group_id
|
||||
main_group
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Rails.version < '6'
|
||||
Rails.configuration.to_prepare do
|
||||
unless User.included_modules.include?(RedmineWorkload::Extensions::UserPatch)
|
||||
User.prepend RedmineWorkload::Extensions::UserPatch
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
class GroupWorkloadPreparer
|
||||
include Redmine::I18n
|
||||
extend Forwardable
|
||||
|
||||
def_delegators :data, :user_workload, :time_span
|
||||
|
||||
attr_reader :data, :params
|
||||
|
||||
def initialize(data:, params:)
|
||||
self.data = data
|
||||
self.params = params
|
||||
end
|
||||
|
||||
def group_workload
|
||||
data.by_group
|
||||
end
|
||||
|
||||
def type(assignee)
|
||||
case assignee.class.name
|
||||
when 'Group'
|
||||
l(:label_aggregation)
|
||||
else
|
||||
assignee.type
|
||||
end
|
||||
end
|
||||
|
||||
def main_group(assignee)
|
||||
case assignee.class.name
|
||||
when 'User'
|
||||
user_group = assignee.main_group_id
|
||||
assignee.groups.find_by(id: user_group)&.name
|
||||
when 'GroupUserDummy'
|
||||
assignee.main_group&.name
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_writer :data, :params
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RedmineWorkload
|
||||
module Hooks
|
||||
class AfterPluginsLoadedHook < Redmine::Hook::Listener
|
||||
def after_plugins_loaded(_context = {})
|
||||
return unless Rails.version > '6'
|
||||
|
||||
patch = RedmineWorkload::Extensions::UserPatch
|
||||
klass = User
|
||||
klass.prepend patch unless klass.included_modules.include?(patch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
class UserWorkloadPreparer
|
||||
include Redmine::I18n
|
||||
extend Forwardable
|
||||
|
||||
def_delegators :data, :time_span
|
||||
|
||||
attr_reader :data, :params
|
||||
|
||||
def initialize(data:, params:)
|
||||
self.data = data
|
||||
self.params = params
|
||||
end
|
||||
|
||||
def group_workload
|
||||
{}
|
||||
end
|
||||
|
||||
def user_workload
|
||||
data.by_user
|
||||
end
|
||||
|
||||
def type(assignee)
|
||||
assignee.type
|
||||
end
|
||||
|
||||
def main_group(assignee)
|
||||
user_group = assignee.main_group_id
|
||||
assignee.groups.find_by(id: user_group)&.name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_writer :data, :params
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WlCalculationRestrictions
|
||||
def consider_parent_issues?
|
||||
settings['workload_of_parent_issues'].present?
|
||||
end
|
||||
|
||||
def settings
|
||||
Setting.plugin_redmine_workload
|
||||
end
|
||||
end
|
||||
108
plugins/redmine_workload/lib/redmine_workload/wl_csv_exporter.rb
Normal file
108
plugins/redmine_workload/lib/redmine_workload/wl_csv_exporter.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
class WlCsvExporter
|
||||
include Redmine::I18n
|
||||
extend Forwardable
|
||||
|
||||
def_delegators :data, :group_workload, :user_workload, :type, :time_span, :main_group
|
||||
|
||||
attr_reader :data, :params
|
||||
|
||||
def initialize(data:, params:)
|
||||
self.data = initialize_data_object(data)
|
||||
self.params = params
|
||||
end
|
||||
|
||||
def header_fields
|
||||
static_column_names.map { |column| l("field_#{column}") } |
|
||||
dynamic_column_names
|
||||
end
|
||||
|
||||
def line(assignee, workload, status)
|
||||
send("#{status}_line", assignee, workload)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_writer :data, :params
|
||||
|
||||
def initialize_data_object(data)
|
||||
return unless data
|
||||
|
||||
klass = data.class
|
||||
"#{klass}Preparer".constantize.new(data: data, params: params)
|
||||
end
|
||||
|
||||
def planned_line(assignee, workload)
|
||||
[l(:label_planned),
|
||||
type(assignee),
|
||||
name(assignee),
|
||||
main_group(assignee),
|
||||
overdue_issues(workload),
|
||||
overdue_hours(workload),
|
||||
unscheduled_issues(workload),
|
||||
unscheduled_hours(workload),
|
||||
workload_over_time(workload)].flatten
|
||||
end
|
||||
|
||||
def available_line(assignee, workload)
|
||||
[l(:label_available),
|
||||
type(assignee),
|
||||
name(assignee),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
max_capacities_over_time(workload)].flatten
|
||||
end
|
||||
|
||||
def name(assignee)
|
||||
assignee.name
|
||||
end
|
||||
|
||||
def overdue_issues(workload)
|
||||
workload[:overdue_number]
|
||||
end
|
||||
|
||||
def overdue_hours(workload)
|
||||
workload[:overdue_hours]
|
||||
end
|
||||
|
||||
def unscheduled_issues(workload)
|
||||
workload[:unscheduled_number]
|
||||
end
|
||||
|
||||
def unscheduled_hours(workload)
|
||||
workload[:unscheduled_hours]
|
||||
end
|
||||
|
||||
def workload_over_time(workload)
|
||||
data.time_span.map do |day|
|
||||
workload[:total][day][:hours]
|
||||
end
|
||||
end
|
||||
|
||||
def max_capacities_over_time(workload)
|
||||
data.time_span.map do |day|
|
||||
workload[:total][day][:highload]
|
||||
end
|
||||
end
|
||||
|
||||
def static_column_names
|
||||
%w[ status
|
||||
type
|
||||
name
|
||||
main_group
|
||||
number_of_overdue_issues
|
||||
number_of_overdue_hours
|
||||
number_of_unscheduled_issues
|
||||
number_of_unscheduled_hours]
|
||||
end
|
||||
|
||||
def dynamic_column_names
|
||||
data.time_span.map { |date| format_date(date) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'wl_user_data_defaults'
|
||||
|
||||
class WlDateTools
|
||||
extend WlUserDataDefaults
|
||||
##
|
||||
# Returns an array with one entry for each month in the given time span.
|
||||
# Each entry is a hash with two keys: :first_day and :last_day, having the
|
||||
# first resp. last day of that month from the time span as value.
|
||||
# @param time_span [Range] Time span
|
||||
# @return [Array(Hash)] Array with one entry for each month in the given time span
|
||||
#
|
||||
def self.months_in_time_span(time_span)
|
||||
raise ArgumentError unless time_span.is_a?(Range)
|
||||
|
||||
# Abort if the given time span is empty.
|
||||
return [] unless time_span.any?
|
||||
|
||||
first_of_current_month = time_span.first
|
||||
last_of_current_month = [first_of_current_month.end_of_month, time_span.last].min
|
||||
|
||||
result = []
|
||||
while first_of_current_month <= time_span.last
|
||||
result.push({
|
||||
first_day: first_of_current_month,
|
||||
last_day: last_of_current_month
|
||||
})
|
||||
|
||||
first_of_current_month = first_of_current_month.beginning_of_month.next_month
|
||||
last_of_current_month = [first_of_current_month.end_of_month, time_span.last].min
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Returns a list of all regular working weekdays.
|
||||
# 1 is monday, 7 is sunday (same as in Date::cwday)
|
||||
def self.working_days
|
||||
result = Set.new
|
||||
|
||||
result.add(1) if settings['general_workday_monday'] != ''
|
||||
result.add(2) if settings['general_workday_tuesday'] != ''
|
||||
result.add(3) if settings['general_workday_wednesday'] != ''
|
||||
result.add(4) if settings['general_workday_thursday'] != ''
|
||||
result.add(5) if settings['general_workday_friday'] != ''
|
||||
result.add(6) if settings['general_workday_saturday'] != ''
|
||||
result.add(7) if settings['general_workday_sunday'] != ''
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def self.working_days_in_time_span(time_span, assignee, no_cache: false)
|
||||
raise ArgumentError unless time_span.is_a?(Range)
|
||||
|
||||
Rails.cache.clear if no_cache
|
||||
|
||||
Rails.cache.fetch("#{assignee.id}/#{time_span}", expires_in: 12.hours) do
|
||||
result = Set.new
|
||||
|
||||
time_span.each do |day|
|
||||
next if vacation?(day, assignee)
|
||||
next if holiday?(day)
|
||||
|
||||
result.add(day) if working_days.include?(day.cwday)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def self.real_distance_in_days(time_span, assignee)
|
||||
raise ArgumentError unless time_span.is_a?(Range)
|
||||
|
||||
working_days_in_time_span(time_span, assignee).size
|
||||
end
|
||||
|
||||
def self.holiday?(day)
|
||||
!WlNationalHoliday.where('start <= ? AND end >= ?', day, day).empty?
|
||||
end
|
||||
|
||||
def self.vacation?(day, assignee)
|
||||
return false unless assignee.is_a?(User)
|
||||
|
||||
!WlUserVacation.where('user_id = ? AND date_from <= ? AND date_to >= ?', assignee.id, day, day).empty?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WlIssueQuery
|
||||
include WlCalculationRestrictions
|
||||
##
|
||||
# Returns all issues that fulfill the following conditions:
|
||||
# * They are open
|
||||
# * The project they belong to is active
|
||||
# * They have no children if consider_parent_issues? is false
|
||||
# * They are parent or child isse if consider_parent_issues? is true
|
||||
#
|
||||
# @param users [Array(User)] An array of user objects.
|
||||
# @return [Array(Issue)] The set of issues meeting the conditions above.
|
||||
#
|
||||
def open_issues_for_users(users, issues = nil)
|
||||
return issues if issues
|
||||
|
||||
raise ArgumentError unless users.is_a?(Array)
|
||||
|
||||
user_ids = users.map(&:id)
|
||||
|
||||
issue = Issue.arel_table
|
||||
project = Project.arel_table
|
||||
issue_status = IssueStatus.arel_table
|
||||
|
||||
# Fetch all issues that ...
|
||||
issues = Issue.joins(:project)
|
||||
.joins(:status)
|
||||
.joins(:assigned_to)
|
||||
.where(issue[:assigned_to_id].in(user_ids)) # Are assigned to one of the interesting users
|
||||
.where(project[:status].eq(1)) # Do not belong to an inactive project
|
||||
.where(issue_status[:is_closed].eq(false)) # Is open
|
||||
|
||||
# Filter out all issues that have children
|
||||
return issues.select(&:leaf?) unless consider_parent_issues?
|
||||
|
||||
# Contains parent and child issues
|
||||
issues.split.flatten
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WlIssueState
|
||||
##
|
||||
# Redefines the overdue state of an issue. Instead of comparing issue.due_date
|
||||
# with the User.current.today (as in Issue#overdue?) it will compare by the
|
||||
# date given.
|
||||
#
|
||||
def issue_overdue?(issue, date)
|
||||
issue.due_date.present? && (issue.due_date < date) && !issue.closed?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Provides the default values for WlUserData object
|
||||
#
|
||||
module WlUserDataDefaults
|
||||
def default_attributes
|
||||
{ threshold_lowload_min: settings['threshold_lowload_min'],
|
||||
threshold_normalload_min: settings['threshold_normalload_min'],
|
||||
threshold_highload_min: settings['threshold_highload_min'] }
|
||||
end
|
||||
|
||||
def settings
|
||||
Setting['plugin_redmine_workload']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
##
|
||||
# Finder method for WLUserData
|
||||
#
|
||||
module WlUserDataFinder
|
||||
##
|
||||
# Finds the workload data of the current user or creates them if not found.
|
||||
# When @user_workload_data is a new record default values will be assigned.
|
||||
#
|
||||
def find_user_workload_data(user_id = User.current.id)
|
||||
@user_workload_data = WlUserData.find_or_create_by(user_id: user_id)
|
||||
@user_workload_data.update_to_defaults_when_new
|
||||
@user_workload_data
|
||||
end
|
||||
end
|
||||
BIN
plugins/redmine_workload/screenshots/group-workload-example.png
Normal file
BIN
plugins/redmine_workload/screenshots/group-workload-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
plugins/redmine_workload/screenshots/workload_calculation.png
Normal file
BIN
plugins/redmine_workload/screenshots/workload_calculation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
34
plugins/redmine_workload/test/authenticate_user.rb
Executable file
34
plugins/redmine_workload/test/authenticate_user.rb
Executable file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RedmineWorkload
|
||||
##
|
||||
# Provide user login test
|
||||
#
|
||||
module AuthenticateUser
|
||||
def log_user(login, password)
|
||||
login_page
|
||||
log_user_in(login, password)
|
||||
assert_equal login, User.find(user_session_id).login
|
||||
end
|
||||
|
||||
module_function
|
||||
|
||||
def login_page
|
||||
User.anonymous
|
||||
get '/login'
|
||||
assert_nil user_session_id
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
def user_session_id
|
||||
session[:user_id]
|
||||
end
|
||||
|
||||
def log_user_in(login, password)
|
||||
post '/login', params: {
|
||||
username: login,
|
||||
password: password
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WlNationalHolidayControllerTest < ActionDispatch::IntegrationTest
|
||||
include RedmineWorkload::AuthenticateUser
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
test 'should get index' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get wl_national_holiday_index_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'should not create new holiday when user is not allowed to' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
post wl_national_holiday_index_path
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should create new national holiday' do
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_national_holiday
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
post wl_national_holiday_index_path,
|
||||
params: { wl_national_holiday:
|
||||
{ start: today, end: tomorrow, reason: 'Eastern' } }
|
||||
assert_redirected_to wl_national_holiday_index_path
|
||||
end
|
||||
|
||||
test 'should not update holiday when user is not allowed to' do
|
||||
holiday = generate_holiday
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_national_holiday_path(id: holiday.id),
|
||||
params: { wl_national_holiday: { reason: 'New Year' } }
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should update holiday' do
|
||||
holiday = generate_holiday
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_national_holiday
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_national_holiday_path(id: holiday.id),
|
||||
params: { wl_national_holiday: { reason: 'New Year' } }
|
||||
assert_redirected_to wl_national_holiday_index_path
|
||||
end
|
||||
|
||||
test 'should not destroy holiday when user is not allowed to' do
|
||||
holiday = generate_holiday
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
delete wl_national_holiday_path(id: holiday.id)
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should destroy holiday' do
|
||||
holiday = generate_holiday
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_national_holiday
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
delete wl_national_holiday_path(id: holiday.id)
|
||||
assert_redirected_to wl_national_holiday_index_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_holiday
|
||||
WlNationalHoliday.create(start: today, end: tomorrow, reason: 'Christmas')
|
||||
end
|
||||
|
||||
def today
|
||||
Time.zone.today
|
||||
end
|
||||
|
||||
def tomorrow
|
||||
today + 1
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WlUserDatasControllerTest < ActionDispatch::IntegrationTest
|
||||
include RedmineWorkload::AuthenticateUser
|
||||
include WlUserDataFinder
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
def setup
|
||||
find_user_workload_data
|
||||
@group = Group.generate!
|
||||
end
|
||||
|
||||
test 'should render edit' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get edit_wl_user_data_path(@user_workload_data)
|
||||
assert_response :success
|
||||
assert_match(/jsmith/, response.body)
|
||||
end
|
||||
|
||||
test 'should update data if user allowed to' do
|
||||
jsmith = users :users_002
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_user_data
|
||||
jsmith.groups << @group
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_user_data_path(@user_workload_data),
|
||||
params: { wl_user_data: to_be_updated(@group.id) }
|
||||
|
||||
assert_redirected_to controller: :workloads, action: :index
|
||||
wl_user_data = WlUserData.find_by(user_id: jsmith.id)
|
||||
current = wl_user_data.attributes
|
||||
expected = { 'id' => wl_user_data.id,
|
||||
'user_id' => jsmith.id,
|
||||
'threshold_lowload_min' => 2.0,
|
||||
'threshold_normalload_min' => 4.0,
|
||||
'threshold_highload_min' => 6.0,
|
||||
'main_group' => @group.id }
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should not update data if user not allowed to' do
|
||||
jsmith = users :users_002
|
||||
jsmith.groups << @group
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_user_data_path(@user_workload_data),
|
||||
params: { wl_user_data: to_be_updated(@group.id) }
|
||||
|
||||
assert :forbidden
|
||||
end
|
||||
|
||||
test 'should render errors messages when user updates with foreign group' do
|
||||
jsmith = users :users_002
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_user_data
|
||||
jsmith.groups << @group
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_user_data_path(@user_workload_data),
|
||||
params: { wl_user_data: to_be_updated(1) }
|
||||
|
||||
assert :success
|
||||
# Use this assertion when error rendering is refactored!
|
||||
assert_select_error(/is not included in the list/)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_be_updated(group_id)
|
||||
{ threshold_lowload_min: 2,
|
||||
threshold_normalload_min: 4,
|
||||
threshold_highload_min: 6,
|
||||
main_group: group_id }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WlUserVacationsControllerTest < ActionDispatch::IntegrationTest
|
||||
include RedmineWorkload::AuthenticateUser
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
test 'should get index' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get wl_user_vacations_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'should not create new vacation when user is not allowed to' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
post wl_user_vacations_path,
|
||||
params: { wl_user_vacations:
|
||||
{ date_from: today, date_to: tomorrow, comment: 'Eastern' } }
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should create new vacation' do
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_user_vacations
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
post wl_user_vacations_path,
|
||||
params: { wl_user_vacations:
|
||||
{ date_from: today, date_to: tomorrow, comment: 'Eastern' } }
|
||||
assert_redirected_to wl_user_vacations_path
|
||||
end
|
||||
|
||||
test 'should not update vacation when user is not allowed to' do
|
||||
vacation = generate_vacation
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_user_vacation_path(id: vacation.id),
|
||||
params: { wl_user_vacation: { comment: 'No comment' } }
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should update vacation' do
|
||||
vacation = generate_vacation
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_user_vacations
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
patch wl_user_vacation_path(id: vacation.id),
|
||||
params: { wl_user_vacation: { comment: 'No comment' } }
|
||||
assert_redirected_to wl_user_vacations_path
|
||||
end
|
||||
|
||||
test 'should not destroy vacation when user is not allowed to' do
|
||||
vacation = generate_vacation
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
delete wl_user_vacation_path(id: vacation.id)
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should destroy vacation' do
|
||||
vacation = generate_vacation
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :edit_user_vacations
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
delete wl_user_vacation_path(id: vacation.id)
|
||||
assert_redirected_to wl_user_vacations_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_vacation
|
||||
WlUserVacation.create(user_id: 2, date_from: today, date_to: tomorrow, comments: 'Private')
|
||||
end
|
||||
|
||||
def today
|
||||
Time.zone.today
|
||||
end
|
||||
|
||||
def tomorrow
|
||||
today + 1
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WorkloadsControllerTest < ActionDispatch::IntegrationTest
|
||||
include RedmineWorkload::AuthenticateUser
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
test 'should not get index when not allowed to' do
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get workloads_path
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test 'should get index' do
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :view_all_workloads
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get workloads_path
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test 'should get index with format csv' do
|
||||
manager = roles :roles_001
|
||||
manager.add_permission! :view_all_workloads
|
||||
log_user('jsmith', 'jsmith')
|
||||
|
||||
get workloads_path(format: 'csv')
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../test_helper', __dir__)
|
||||
|
||||
class RoutingWlUserDataTest < Redmine::RoutingTest
|
||||
def test_wl_user_data
|
||||
should_route 'GET /wl_user_datas/1/edit' => 'wl_user_datas#edit', id: '1'
|
||||
should_route 'PATCH /wl_user_datas/1' => 'wl_user_datas#update', id: '1'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../test_helper', __dir__)
|
||||
|
||||
class RoutingWorkloadTest < Redmine::RoutingTest
|
||||
def test_workloads
|
||||
should_route 'GET /workloads' => 'workloads#index'
|
||||
end
|
||||
end
|
||||
7
plugins/redmine_workload/test/test_helper.rb
Normal file
7
plugins/redmine_workload/test/test_helper.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Load the normal Rails helper
|
||||
require File.expand_path('../../../test/test_helper', __dir__)
|
||||
# Load other test helper modules
|
||||
require File.expand_path('authenticate_user', __dir__)
|
||||
require File.expand_path('workload_object_helper', __dir__)
|
||||
59
plugins/redmine_workload/test/unit/group_selection_test.rb
Normal file
59
plugins/redmine_workload/test/unit/group_selection_test.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WlGroupSelectionTest < ActiveSupport::TestCase
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
def setup
|
||||
Group.where.not(id: [12, 13]).delete_all
|
||||
@build_in_groups = Group.where(id: [12, 13])
|
||||
@groups = 5.times.map { |count| Group.generate! if count }
|
||||
end
|
||||
|
||||
test 'should return all groups if the current user is admin' do
|
||||
admin = users :users_001 # admin
|
||||
groups = WlGroupSelection.new(user: admin)
|
||||
expected = (@groups.map(&:id) | @build_in_groups.map(&:id)).uniq.sort
|
||||
current = groups.allowed_to_display.map(&:id).sort
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return all groups when user has permission :view_all_workloads' do
|
||||
current_user = users :users_002 # jsmith
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_all_workloads
|
||||
groups = WlGroupSelection.new(user: current_user)
|
||||
expected = (@groups.map(&:id) | @build_in_groups.map(&:id)).uniq.sort
|
||||
current = groups.allowed_to_display.map(&:id).sort
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return current users groups when allowed to :view_own_group_workloads' do
|
||||
group1 = Group.generate!
|
||||
group2 = Group.generate!
|
||||
group3 = Group.generate!
|
||||
user1 = User.generate!
|
||||
user1.groups << group1
|
||||
user2 = User.generate!
|
||||
user2.groups << group2
|
||||
user3 = User.generate!
|
||||
user3.groups << group3
|
||||
|
||||
current_user = users :users_002 # jsmith
|
||||
current_user.groups << [group1, group3]
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_own_group_workloads
|
||||
groups = WlGroupSelection.new(user: current_user)
|
||||
expected = [group1, group3].map(&:id).sort
|
||||
current = groups.allowed_to_display.map(&:id).sort
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return an empty array if the current user has no permission to view workloads' do
|
||||
groups = WlGroupSelection.new(user: User.anonymous)
|
||||
|
||||
assert_equal [], groups.allowed_to_display
|
||||
end
|
||||
end
|
||||
104
plugins/redmine_workload/test/unit/group_user_dummy_test.rb
Normal file
104
plugins/redmine_workload/test/unit/group_user_dummy_test.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class GroupUserDummyTest < ActiveSupport::TestCase
|
||||
include WlUserDataFinder
|
||||
include WorkloadsHelper
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
def setup
|
||||
@group = Group.generate!
|
||||
@user1 = User.generate!
|
||||
@user1.groups << @group
|
||||
@user1_wl_data = find_user_workload_data(@user1.id)
|
||||
@user1_wl_data.main_group = @group.id
|
||||
@user1_wl_data.save
|
||||
@user2 = User.generate!
|
||||
@user2.groups << @group
|
||||
@user2_wl_data = find_user_workload_data(@user2.id)
|
||||
@user2_wl_data.main_group = @group.id
|
||||
@user2_wl_data.save
|
||||
@dummy = GroupUserDummy.new(group: @group)
|
||||
end
|
||||
|
||||
test 'should respond to group' do
|
||||
assert @dummy.respond_to? :group
|
||||
end
|
||||
|
||||
test 'should respond to lastname' do
|
||||
assert @dummy.respond_to? :lastname
|
||||
end
|
||||
|
||||
test 'should respond to name' do
|
||||
assert @dummy.respond_to? :name
|
||||
end
|
||||
|
||||
test 'should respond to main_group' do
|
||||
assert @dummy.respond_to? :main_group
|
||||
end
|
||||
|
||||
test 'should respond to type' do
|
||||
assert @dummy.respond_to? :type
|
||||
end
|
||||
|
||||
test 'should respond to threshold_lowload_min' do
|
||||
assert @dummy.respond_to? :threshold_lowload_min
|
||||
end
|
||||
|
||||
test 'should respond to threshold_normalload_min' do
|
||||
assert @dummy.respond_to? :threshold_normalload_min
|
||||
end
|
||||
|
||||
test 'should respond to threshold_highload_min' do
|
||||
assert @dummy.respond_to? :threshold_highload_min
|
||||
end
|
||||
|
||||
test 'should sum up threshold of group members when unset' do
|
||||
group = Group.generate!
|
||||
user1 = User.generate!
|
||||
user1.groups << group
|
||||
user1_wl_data = WlUserData.new(user_id: user1.id)
|
||||
user1_wl_data.main_group = group.id
|
||||
user1_wl_data.save
|
||||
user2 = User.generate!
|
||||
user2.groups << group
|
||||
user2_wl_data = WlUserData.new(user_id: user2.id)
|
||||
user2_wl_data.main_group = group.id
|
||||
user2_wl_data.save
|
||||
dummy = GroupUserDummy.new(group: group)
|
||||
expected = 0.0
|
||||
thresholds = %i[threshold_lowload_min threshold_normalload_min threshold_highload_min]
|
||||
thresholds.each do |threshold|
|
||||
current = dummy.send :sum_up, threshold
|
||||
assert_equal expected, current
|
||||
end
|
||||
end
|
||||
|
||||
test 'should sum up thresholds of group members when given' do
|
||||
lowload1, normalload1, highload1 = [2, 4, 6]
|
||||
@user1_wl_data.threshold_lowload_min = lowload1
|
||||
@user1_wl_data.threshold_normalload_min = normalload1
|
||||
@user1_wl_data.threshold_highload_min = highload1
|
||||
@user1_wl_data.save
|
||||
lowload2, normalload2, highload2 = [3, 5, 7]
|
||||
@user2_wl_data.threshold_lowload_min = lowload2
|
||||
@user2_wl_data.threshold_normalload_min = normalload2
|
||||
@user2_wl_data.threshold_highload_min = highload2
|
||||
@user2_wl_data.save
|
||||
|
||||
expected_threshold_lowload_min = lowload1 + lowload2
|
||||
current_threshold_lowload_min = @dummy.send :sum_up, :threshold_lowload_min
|
||||
assert_equal expected_threshold_lowload_min, current_threshold_lowload_min
|
||||
|
||||
expected_threshold_normalload_min = normalload1 + normalload2
|
||||
current_threshold_normalload_min = @dummy.send :sum_up, :threshold_normalload_min
|
||||
assert_equal expected_threshold_normalload_min, current_threshold_normalload_min
|
||||
|
||||
expected_threshold_highload_min = highload1 + highload2
|
||||
current_threshold_highload_min = @dummy.send :sum_up, :threshold_highload_min
|
||||
assert_equal expected_threshold_highload_min, current_threshold_highload_min
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class GroupWorkloadPreparerTest < ActiveSupport::TestCase
|
||||
include Redmine::I18n
|
||||
|
||||
test 'should respond to user_workload' do
|
||||
preparer = GroupWorkloadPreparer.new(data: {}, params: {})
|
||||
assert preparer.respond_to? :user_workload
|
||||
end
|
||||
|
||||
test 'should repsond to time_span' do
|
||||
preparer = GroupWorkloadPreparer.new(data: {}, params: {})
|
||||
assert preparer.respond_to? :time_span
|
||||
end
|
||||
|
||||
test 'should repsond to group_workload' do
|
||||
preparer = GroupWorkloadPreparer.new(data: {}, params: {})
|
||||
assert preparer.respond_to? :group_workload
|
||||
end
|
||||
|
||||
test 'should return type of assignee' do
|
||||
preparer = GroupWorkloadPreparer.new(data: {}, params: {})
|
||||
assert_equal l(:label_aggregation), preparer.type(Group.generate!)
|
||||
assert_equal 'User', preparer.type(User.generate!)
|
||||
end
|
||||
|
||||
test 'should return main group of assignee' do
|
||||
group = Group.generate!
|
||||
dummy = GroupUserDummy.new(group: group)
|
||||
user = User.generate!
|
||||
user.groups << group
|
||||
user.create_wl_user_data(main_group: group.id)
|
||||
preparer = GroupWorkloadPreparer.new(data: {}, params: {})
|
||||
assert_equal group.name, preparer.main_group(user)
|
||||
assert_equal dummy.main_group.name, preparer.main_group(dummy)
|
||||
end
|
||||
end
|
||||
233
plugins/redmine_workload/test/unit/group_workload_test.rb
Normal file
233
plugins/redmine_workload/test/unit/group_workload_test.rb
Normal file
@@ -0,0 +1,233 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class GroupWorkloadTest < ActiveSupport::TestCase
|
||||
include RedmineWorkload::WorkloadObjectHelper
|
||||
fixtures :roles, :projects, :issue_statuses, :trackers, :enumerations, :users
|
||||
|
||||
def setup
|
||||
@user = users :users_001
|
||||
@manager = roles :roles_001
|
||||
@manager.add_permission! :view_all_workloads
|
||||
end
|
||||
|
||||
test 'should respond to by_group' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct)
|
||||
assert group_workload.respond_to?(:by_group)
|
||||
end
|
||||
|
||||
test 'should respond to time_span' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct)
|
||||
assert group_workload.respond_to?(:time_span)
|
||||
end
|
||||
|
||||
test 'should respond to user_workload' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct)
|
||||
assert group_workload.respond_to?(:user_workload)
|
||||
end
|
||||
|
||||
test 'define_group_members should return empty hash when no groups selected' do
|
||||
empty_group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct)
|
||||
expected = {}
|
||||
current = empty_group_workload.send :group_members
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should select group members only once' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct,
|
||||
group_user_dummy_strategy: true)
|
||||
selected_groups = group_workload.send(:selected_groups)
|
||||
group1 = selected_groups.first
|
||||
group2 = selected_groups.last
|
||||
group_members = group_workload.send(:group_members)
|
||||
member_list1 = group_members[group1].keys.map(&:id)
|
||||
member_list2 = group_members[group2].keys.map(&:id)
|
||||
count = (member_list1 | member_list2).count
|
||||
assert_equal 3, count
|
||||
current = member_list1 & member_list2
|
||||
expected = []
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should list group_user_dummy first' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct,
|
||||
group_user_dummy_strategy: true)
|
||||
sorted_user_workload = group_workload.send(:sorted_user_workload)
|
||||
assert sorted_user_workload.keys.first.is_a? GroupUserDummy
|
||||
end
|
||||
|
||||
test 'should return no holiday when only one group member is on vacation for a given day' do
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :same,
|
||||
vacation_strategy: :distinct)
|
||||
|
||||
user1_id = group_workload.send(:users).send(:users).first
|
||||
user1 = User.find(user1_id)
|
||||
assert user1.is_a? User
|
||||
group = Group.find(user1.main_group_id)
|
||||
|
||||
## The following checks are only required for the error analysis if any
|
||||
# assert user1.wl_user_vacations.where(date_from: first_day, date_to: first_day).take.presence
|
||||
# assert WlUserVacation.where(user_id: user1.id, date_from: first_day, date_to: first_day).take.presence
|
||||
|
||||
# user2_id = group_workload.send(:users).send(:users).last
|
||||
# user2 = User.find(user2_id)
|
||||
# assert user2.is_a? User
|
||||
|
||||
# assert_not_equal user1, user2
|
||||
|
||||
# assert_equal group.id, user2.main_group_id
|
||||
# assert user2.wl_user_vacations.where(date_from: last_day, date_to: last_day).take.presence
|
||||
# assert WlUserVacation.where(user_id: user2.id, date_from: last_day, date_to: last_day).take.presence
|
||||
|
||||
# assert_equal 3, group_workload.user_workload.keys.count # GroupUserDummy + user1 + user2
|
||||
# assert_equal 3, group_workload.send(:group_members)[group].keys.count
|
||||
## end
|
||||
|
||||
expected = false
|
||||
current = group_workload.send(:holiday_at, first_day, :total, group)
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return holiday when all group members are on vacation for a given day' do
|
||||
# and no group user dummy exists
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :same,
|
||||
vacation_strategy: :same)
|
||||
user1_id = group_workload.send(:users).send(:users).first
|
||||
user1 = User.find(user1_id)
|
||||
assert user1.is_a? User
|
||||
group = Group.find(user1.main_group_id)
|
||||
|
||||
## The following checks are only required for the error analysis if any
|
||||
# assert user1.wl_user_vacations.where(date_from: first_day, date_to: first_day).take.presence
|
||||
# assert WlUserVacation.where(user_id: user1.id, date_from: first_day, date_to: first_day).take.presence
|
||||
|
||||
# user2_id = group_workload.send(:users).send(:users).last
|
||||
# user2 = User.find(user2_id)
|
||||
# assert user2.is_a? User
|
||||
|
||||
# assert_equal group.id, user2.main_group_id
|
||||
# assert user2.wl_user_vacations.where(date_from: first_day, date_to: first_day).take.presence
|
||||
# assert WlUserVacation.where(user_id: user2.id, date_from: first_day, date_to: first_day).take.presence
|
||||
|
||||
# assert_equal 3, group_workload.user_workload.keys.count # GroupUserDummy + user1 + user2
|
||||
# assert_equal 3, group_workload.send(:group_members)[group].keys.count
|
||||
## end
|
||||
|
||||
expected = true
|
||||
current = group_workload.send(:holiday_at, first_day, :total, group)
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return no holiday when all group members are on holiday except group user dummy' do
|
||||
# group user dummy cannot go to holiday
|
||||
group_workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: groups_defined,
|
||||
main_group_strategy: :same,
|
||||
vacation_strategy: :same,
|
||||
group_user_dummy_strategy: true)
|
||||
|
||||
user1_id = group_workload.send(:users).send(:users).first
|
||||
user1 = User.find(user1_id)
|
||||
assert user1.is_a? User
|
||||
group = Group.find(user1.main_group_id)
|
||||
|
||||
expected = false
|
||||
current = group_workload.send(:holiday_at, first_day, :total, group)
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should count unscheduled issues and hours on group level' do
|
||||
group1, group2 = groups_defined
|
||||
workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: [group1, group2],
|
||||
main_group_strategy: :distinct,
|
||||
vacation_strategy: :distinct,
|
||||
group_user_dummy_strategy: true)
|
||||
|
||||
assert_equal 2, workload.by_group[group1][:unscheduled_number]
|
||||
assert_equal 24.0, workload.by_group[group1][:unscheduled_hours]
|
||||
assert_equal 0, workload.by_group[group2][:unscheduled_number]
|
||||
assert_equal 0.0, workload.by_group[group2][:unscheduled_hours]
|
||||
end
|
||||
|
||||
test 'should calculate day dependent threshold values for group workload without dummy' do
|
||||
group1, group2 = groups_defined
|
||||
workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: [group1, group2],
|
||||
main_group_strategy: :same, # group1
|
||||
# first_day for one user and last_day for the other
|
||||
vacation_strategy: :distinct)
|
||||
|
||||
# threshold default values: highload = 6, lowload = 3, normalload = 4
|
||||
# holiday for one of two group members
|
||||
assert_equal 3.0, workload.send(:threshold_at, first_day, :lowload, group1)
|
||||
assert_equal 4.0, workload.send(:threshold_at, first_day, :normalload, group1)
|
||||
assert_equal 6.0, workload.send(:threshold_at, first_day, :highload, group1)
|
||||
# Saturday
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :lowload, group1)
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :normalload, group1)
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :highload, group1)
|
||||
# both group members are working
|
||||
assert_equal 6.0, workload.send(:threshold_at, first_day + 1, :lowload, group1)
|
||||
assert_equal 8.0, workload.send(:threshold_at, first_day + 1, :normalload, group1)
|
||||
assert_equal 12.0, workload.send(:threshold_at, first_day + 1, :highload, group1)
|
||||
end
|
||||
|
||||
test 'should calculate day dependent threshold values for group workload with dummy' do
|
||||
group1, group2 = groups_defined
|
||||
workload = prepare_group_workload(user: @user,
|
||||
role: @manager,
|
||||
groups: [group1, group2],
|
||||
main_group_strategy: :same, # group1
|
||||
# first_day for one user and last_day for the other
|
||||
vacation_strategy: :distinct,
|
||||
group_user_dummy_strategy: true)
|
||||
|
||||
# threshold default values: highload = 6, lowload = 3, normalload = 4
|
||||
# holiday for one of two group members
|
||||
assert_equal 3.0, workload.send(:threshold_at, first_day, :lowload, group1)
|
||||
assert_equal 4.0, workload.send(:threshold_at, first_day, :normalload, group1)
|
||||
assert_equal 6.0, workload.send(:threshold_at, first_day, :highload, group1)
|
||||
# Saturday
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :lowload, group1)
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :normalload, group1)
|
||||
assert_equal 0.0, workload.send(:threshold_at, first_day + 3, :highload, group1)
|
||||
# both group members are working
|
||||
assert_equal 6.0, workload.send(:threshold_at, first_day + 1, :lowload, group1)
|
||||
assert_equal 8.0, workload.send(:threshold_at, first_day + 1, :normalload, group1)
|
||||
assert_equal 12.0, workload.send(:threshold_at, first_day + 1, :highload, group1)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../../test_helper', __dir__)
|
||||
|
||||
class WlIssueQueryTest < ActiveSupport::TestCase
|
||||
include RedmineWorkload::WorkloadObjectHelper
|
||||
include WlIssueQuery
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
def setup
|
||||
@manager = roles :roles_001
|
||||
@user1 = User.generate!
|
||||
@user2 = User.generate!
|
||||
@project1 = Project.generate!
|
||||
User.add_to_project(@user1, @project1, @manager)
|
||||
User.add_to_project(@user2, @project1, @manager)
|
||||
@status_new = IssueStatus.find(1)
|
||||
@parent_issue = Issue.generate!(assigned_to: @user1,
|
||||
status: @status_new,
|
||||
project: @project1)
|
||||
@child_issue = Issue.generate!(assigned_to: @user1,
|
||||
status: @status_new,
|
||||
project: @project1,
|
||||
parent_issue_id: @parent_issue.id)
|
||||
end
|
||||
|
||||
def teardown
|
||||
@child_issue.destroy
|
||||
@parent_issue.destroy
|
||||
@user1.destroy
|
||||
@user2.destroy
|
||||
@project1.destroy
|
||||
Setting.clear_cache
|
||||
end
|
||||
|
||||
test 'should query parent and child issues if any' do
|
||||
with_plugin_settings 'workload_of_parent_issues' => 'checked' do
|
||||
expected_ids = [@parent_issue.id, @child_issue.id]
|
||||
query_result = open_issues_for_users([@user1, @user2])
|
||||
query_result_ids = query_result.pluck(:id)
|
||||
assert_equal expected_ids, query_result_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
15
plugins/redmine_workload/test/unit/user_patch_test.rb
Normal file
15
plugins/redmine_workload/test/unit/user_patch_test.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class UserPatchTest < ActiveSupport::TestCase
|
||||
test 'should respond to wl_user_data' do
|
||||
user = User.generate!
|
||||
assert user.respond_to? :wl_user_data
|
||||
end
|
||||
|
||||
test 'should repsond to wl_user_vacations' do
|
||||
user = User.generate!
|
||||
assert user.respond_to? :wl_user_vacations
|
||||
end
|
||||
end
|
||||
110
plugins/redmine_workload/test/unit/user_selection_test.rb
Normal file
110
plugins/redmine_workload/test/unit/user_selection_test.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require File.expand_path('../test_helper', __dir__)
|
||||
|
||||
class WlUserSelectionTest < ActiveSupport::TestCase
|
||||
include WlUserDataDefaults
|
||||
|
||||
fixtures :trackers, :projects, :projects_trackers, :members, :member_roles,
|
||||
:users, :issue_statuses, :enumerations, :roles
|
||||
|
||||
def setup
|
||||
@group1 = Group.generate!
|
||||
@group2 = Group.generate!
|
||||
@group3 = Group.generate!
|
||||
@user1 = User.generate!
|
||||
@user1.groups << @group1
|
||||
@user2 = User.generate!
|
||||
@user2.groups << @group2
|
||||
@user3 = User.generate!
|
||||
@user3.groups << @group3
|
||||
@group_member_ids = []
|
||||
@group_member_ids << @group1.users.map(&:id)
|
||||
@group_member_ids << @group2.users.map(&:id)
|
||||
@group_member_ids << @group3.users.map(&:id)
|
||||
@group_member_ids
|
||||
end
|
||||
|
||||
test 'should return all users if the current user is admin' do
|
||||
current_user = User.generate!(admin: true)
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
assert_equal @group_member_ids.flatten.sort, users.allowed_to_display.map(&:id).sort
|
||||
end
|
||||
|
||||
test 'should return all active users when user has permission :view_all_workloads' do
|
||||
current_user = users :users_002 # jsmith
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_all_workloads
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
expected = @group_member_ids.flatten.sort
|
||||
current = users.send(:all_users).map(&:id).sort
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return users of the current users groups when allowed to :view_own_group_workloads' do
|
||||
current_user = users :users_002 # jsmith
|
||||
current_user.groups << @group1
|
||||
current_user.groups << @group3
|
||||
|
||||
current_user.create_wl_user_data(default_attributes.merge(main_group: @group1.id))
|
||||
assert_equal @group1.id, current_user.wl_user_data.main_group
|
||||
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_own_group_workloads
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
expected = [@user1, @user3].map(&:id).sort
|
||||
current = users.allowed_to_display.map(&:id).sort
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should return the current user if allowed to :view_own_workloads' do
|
||||
current_user = users :users_002 # jsmith
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_own_workloads
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
assert_equal [current_user.id], users.allowed_to_display.map(&:id)
|
||||
end
|
||||
|
||||
test 'should return an empty array if the current user has no permission to view workloads' do
|
||||
current_user = User.anonymous
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
|
||||
assert_equal [], users.allowed_to_display
|
||||
end
|
||||
|
||||
test 'should return current user if no other users given' do
|
||||
current_user = users :users_002 # jsmith
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_all_workloads
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [])
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
expected = [current_user]
|
||||
current = users.send(:include_current_user)
|
||||
assert_equal expected, current
|
||||
end
|
||||
|
||||
test 'should not return current user if not selected' do
|
||||
@user1.create_wl_user_data(default_attributes.merge(main_group: @group1.id))
|
||||
current_user = users :users_002 # jsmith
|
||||
manager = roles :roles_001 # manager
|
||||
manager.add_permission! :view_all_workloads
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [@group1.id, @group2.id, @group3.id])
|
||||
users = WlUserSelection.new(user: current_user, group_selection: groups)
|
||||
expected = []
|
||||
current = users.send(:include_current_user)
|
||||
assert_equal expected, current
|
||||
|
||||
groups = WlGroupSelection.new(user: current_user, groups: [])
|
||||
users = WlUserSelection.new(user: current_user, users: [@user1.id, @user2.id, @user3.id], group_selection: groups)
|
||||
expected = []
|
||||
current = users.send(:include_current_user)
|
||||
assert_equal expected, current
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user