diff --git a/Gemfile b/Gemfile
index 6bd5f9366a..52aee1806a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -230,7 +230,7 @@ platforms :jruby do
end
group :opf_plugins do
- gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', tag: 'v5.0.9'
+ gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'stable/5'
end
# TODO: Make this group :optional when bundler v10.x
diff --git a/Gemfile.lock b/Gemfile.lock
index 2b6048d350..56f12409c1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -40,85 +40,134 @@ GIT
omniauth-openid-connect (>= 0.2.0)
GIT
- remote: https://github.com/finnlabs/openproject-announcements.git
- revision: 0227b6a23e02b0d1b83674a1023075af6829ad03
- branch: release/5.0
+ remote: https://github.com/finnlabs/rack-protection.git
+ revision: 5a7d1bd2f05ca75faf7909c8cc978732a0080898
+ ref: 5a7d1bd
+ specs:
+ rack-protection (1.5.2)
+ rack
+
+GIT
+ remote: https://github.com/finnlabs/rspec-example_disabler.git
+ revision: deb9c38e3f4e3688724583ac1ff58e1ae8aba409
+ specs:
+ rspec-example_disabler (0.0.1)
+
+GIT
+ remote: https://github.com/finnlabs/transactional_lock.git
+ revision: 6948b1d446db0da5645e68ffeeddca1c4944c3bc
+ branch: master
+ specs:
+ transactional_lock (0.1.0)
+ activerecord (>= 4.0)
+
+GIT
+ remote: https://github.com/opf/openproject-translations.git
+ revision: c8772c56c2f28b0fe7d489b0f49147cf8caadaad
+ branch: stable/5
+ specs:
+ openproject-translations (5.0.10)
+ crowdin-api (~> 0.4.0)
+ mixlib-shellout (~> 2.1.0)
+ rails (~> 4.2.3)
+ rubyzip
+
+GIT
+ remote: https://github.com/rails/prototype-rails.git
+ revision: 0fed929ff48c10c3b978edd3baa983a81f404dbf
+ branch: 4.2
+ specs:
+ prototype-rails (4.0.0)
+ rails (~> 4.0)
+
+GIT
+ remote: https://github.com/rails/prototype_legacy_helper.git
+ revision: a2cd95c3e3c1a4f7a9566efdab5ce59c886cb05f
+ specs:
+ prototype_legacy_helper (0.0.0)
+
+GIT
+ remote: https://github.com/rspec/rspec-activemodel-mocks
+ revision: 947a171de990f3056c2ad8b58922298339bc123e
+ specs:
+ rspec-activemodel-mocks (1.0.2)
+ activemodel (>= 3.0)
+ activesupport (>= 3.0)
+ rspec-mocks (>= 2.99, < 4.0)
+
+PATH
+ remote: vendored-plugins/openproject-announcements
specs:
openproject-announcements (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-backlogs.git
- revision: 1f5885d98225b84948735408f02b9d75ddda94f0
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-auth_plugins
+ specs:
+ openproject-auth_plugins (5.0.10)
+ omniauth (~> 1.0)
+ rails (~> 4.2.4)
+
+PATH
+ remote: vendored-plugins/openproject-backlogs
specs:
openproject-backlogs (5.0.10)
acts_as_silent_list (~> 2.0.0)
openproject-pdf_export
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-costs.git
- revision: 081633d0080ab530b1aca14a0c5a1d1de990bb94
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-costs
specs:
openproject-costs (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-github_integration
- revision: 531a8bc565333513cf9ec74f42b1b3d5f016c379
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-documents
+ specs:
+ openproject-documents (5.0.10)
+ rails (~> 4.2.4)
+
+PATH
+ remote: vendored-plugins/openproject-github_integration
specs:
openproject-github_integration (5.0.10)
openproject-webhooks (~> 5.0.1)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-global_roles.git
- revision: 49ffcaa65bb670cc4df01cd674f9d22efd65cfbb
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-global_roles
specs:
openproject-global_roles (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-help_link.git
- revision: 41505497b1553fa30966b991c5e9c2eac30270fd
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-help_link
specs:
openproject-help_link (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-local_avatars
- revision: 4224c3f2ff6b25460413ea975ad06364ee125aa9
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-local_avatars
specs:
openproject-local_avatars (5.0.10)
rails (~> 4.2.4)
rmagick (~> 2.15.4)
-GIT
- remote: https://github.com/finnlabs/openproject-meeting.git
- revision: 6ccb0da6e4c8240d9334e611c410b21abdf078c2
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-meeting
specs:
openproject-meeting (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-my_project_page.git
- revision: 185218356fb7348a3763a92b1cc39713e8fb2273
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-my_project_page
specs:
openproject-my_project_page (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-openid_connect.git
- revision: e3d32394b8d042cdfca195d5f1e2e694d4181099
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-openid_connect
specs:
openproject-openid_connect (5.0.10)
lobby_boy (~> 0.1)
@@ -126,132 +175,47 @@ GIT
openproject-auth_plugins (~> 5.0.1)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-pdf_export.git
- revision: 937400760f70cb6a4d362d70f850ec1e6c1d1908
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-pdf_export
specs:
openproject-pdf_export (5.0.10)
prawn (~> 0.14.0)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-reporting.git
- revision: 944f251962fd5c89b6a968efd22b5b49de3f6d94
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-reporting
specs:
openproject-reporting (5.0.10)
openproject-costs (>= 5.0.1)
rails (~> 4.2.4)
reporting_engine (>= 1.1.0)
-GIT
- remote: https://github.com/finnlabs/openproject-themes-dark.git
- revision: 09f50ff82489390910a2afe1d6affeccda09c6c0
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-themes-dark
specs:
openproject-themes-dark (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-webhooks
- revision: 23edcf48401f075407a489824688ada593f5c3c9
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-webhooks
specs:
openproject-webhooks (5.0.10)
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/openproject-xls_export.git
- revision: cdde695086e1d35e0d2a11dbecd412eb793ad120
- branch: release/5.0
+PATH
+ remote: vendored-plugins/openproject-xls_export
specs:
openproject-xls_export (5.0.10)
rails (~> 4.2.4)
spreadsheet (~> 0.8.9)
-GIT
- remote: https://github.com/finnlabs/rack-protection.git
- revision: 5a7d1bd2f05ca75faf7909c8cc978732a0080898
- ref: 5a7d1bd
- specs:
- rack-protection (1.5.2)
- rack
-
-GIT
- remote: https://github.com/finnlabs/reporting_engine.git
- revision: f4ba5beb1f1413b9c66bf5a3e458b547202dc4ee
- branch: dev
+PATH
+ remote: vendored-plugins/reporting_engine
specs:
- reporting_engine (1.2.0)
+ reporting_engine (5.0.10)
json
rails (~> 4.2.4)
-GIT
- remote: https://github.com/finnlabs/rspec-example_disabler.git
- revision: deb9c38e3f4e3688724583ac1ff58e1ae8aba409
- specs:
- rspec-example_disabler (0.0.1)
-
-GIT
- remote: https://github.com/finnlabs/transactional_lock.git
- revision: 6948b1d446db0da5645e68ffeeddca1c4944c3bc
- branch: master
- specs:
- transactional_lock (0.1.0)
- activerecord (>= 4.0)
-
-GIT
- remote: https://github.com/opf/openproject-auth_plugins.git
- revision: 75851e32206bc34fb79880fb1e0bb63f93a2f3e0
- branch: release/5.0
- specs:
- openproject-auth_plugins (5.0.10)
- omniauth (~> 1.0)
- rails (~> 4.2.4)
-
-GIT
- remote: https://github.com/opf/openproject-documents.git
- revision: 522c6fefd4612b94387d817cbb20c841143df6e7
- branch: release/5.0
- specs:
- openproject-documents (5.0.10)
- rails (~> 4.2.4)
-
-GIT
- remote: https://github.com/opf/openproject-translations.git
- revision: e5ecc7e6bd7b0c0c65ae382ae752cf747bb1d0b0
- branch: release/5.0
- specs:
- openproject-translations (5.0.10)
- crowdin-api (~> 0.4.0)
- mixlib-shellout (~> 2.1.0)
- rails (~> 4.2.3)
- rubyzip
-
-GIT
- remote: https://github.com/rails/prototype-rails.git
- revision: 0fed929ff48c10c3b978edd3baa983a81f404dbf
- branch: 4.2
- specs:
- prototype-rails (4.0.0)
- rails (~> 4.0)
-
-GIT
- remote: https://github.com/rails/prototype_legacy_helper.git
- revision: a2cd95c3e3c1a4f7a9566efdab5ce59c886cb05f
- specs:
- prototype_legacy_helper (0.0.0)
-
-GIT
- remote: https://github.com/rspec/rspec-activemodel-mocks
- revision: 947a171de990f3056c2ad8b58922298339bc123e
- specs:
- rspec-activemodel-mocks (1.0.2)
- activemodel (>= 3.0)
- activesupport (>= 3.0)
- rspec-mocks (>= 2.99, < 4.0)
-
GEM
remote: https://rubygems.org/
specs:
@@ -929,4 +893,4 @@ DEPENDENCIES
will_paginate (~> 3.0)
BUNDLED WITH
- 1.10.6
+ 1.11.2
diff --git a/Gemfile.plugins b/Gemfile.plugins
index 7e3bf5eaae..9351a23ea2 100644
--- a/Gemfile.plugins
+++ b/Gemfile.plugins
@@ -1,4 +1,4 @@
-CORE_VERSION = 'v5.0.9'
+CORE_VERSION = 'stable/5'
# the dependencies from the gemspec from a git repo are ignored
# see also https://github.com/bundler/bundler/issues/1041
@@ -9,31 +9,31 @@ gem "pdf-inspector", "~>1.0.0", group: [:development, :test]
# DO NOT CHANGE THE ORDER OF PLUGINS
#
group :opf_plugins do
- gem 'openproject-global_roles', git: "https://github.com/finnlabs/openproject-global_roles.git", tag: CORE_VERSION
+ gem "openproject-global_roles", path: "vendored-plugins/openproject-global_roles"
- gem 'openproject-auth_plugins', git: 'https://github.com/opf/openproject-auth_plugins.git', tag: CORE_VERSION
+ gem "openproject-auth_plugins", path: "vendored-plugins/openproject-auth_plugins"
gem 'omniauth-openid_connect-providers', git: 'https://github.com/finnlabs/omniauth-openid_connect-providers.git', branch: 'dev'
gem 'omniauth-openid-connect', git: 'https://github.com/finnlabs/omniauth-openid-connect.git', branch: 'dev'
- gem 'openproject-openid_connect', git: 'https://github.com/finnlabs/openproject-openid_connect.git', tag: CORE_VERSION
+ gem "openproject-openid_connect", path: "vendored-plugins/openproject-openid_connect"
- gem 'openproject-documents', git: 'https://github.com/opf/openproject-documents.git', tag: CORE_VERSION
+ gem "openproject-documents", path: "vendored-plugins/openproject-documents"
- gem 'openproject-help_link', git: 'https://github.com/finnlabs/openproject-help_link.git', tag: CORE_VERSION
- gem 'openproject-announcements', git: "https://github.com/finnlabs/openproject-announcements.git", tag: CORE_VERSION
- gem 'openproject-my_project_page', git: 'https://github.com/finnlabs/openproject-my_project_page.git', tag: CORE_VERSION
- gem 'openproject-xls_export', git: "https://github.com/finnlabs/openproject-xls_export.git", tag: CORE_VERSION
+ gem "openproject-help_link", path: "vendored-plugins/openproject-help_link"
+ gem "openproject-announcements", path: "vendored-plugins/openproject-announcements"
+ gem "openproject-my_project_page", path: "vendored-plugins/openproject-my_project_page"
+ gem "openproject-xls_export", path: "vendored-plugins/openproject-xls_export"
- gem 'reporting_engine', git: 'https://github.com/finnlabs/reporting_engine.git', branch: 'dev'
- gem 'openproject-costs', git: 'https://github.com/finnlabs/openproject-costs.git', tag: CORE_VERSION
- gem 'openproject-reporting', git: 'https://github.com/finnlabs/openproject-reporting.git', tag: CORE_VERSION
+ gem "reporting_engine", path: "vendored-plugins/reporting_engine"
+ gem "openproject-costs", path: "vendored-plugins/openproject-costs"
+ gem "openproject-reporting", path: "vendored-plugins/openproject-reporting"
- gem 'openproject-meeting', git: 'https://github.com/finnlabs/openproject-meeting.git', tag: CORE_VERSION
- gem 'openproject-pdf_export', git: 'https://github.com/finnlabs/openproject-pdf_export.git', tag: CORE_VERSION
+ gem "openproject-meeting", path: "vendored-plugins/openproject-meeting"
+ gem "openproject-pdf_export", path: "vendored-plugins/openproject-pdf_export"
- gem "openproject-backlogs", git: "https://github.com/finnlabs/openproject-backlogs.git", tag: CORE_VERSION
+ gem "openproject-backlogs", path: "vendored-plugins/openproject-backlogs"
- gem 'openproject-themes-dark', git: "https://github.com/finnlabs/openproject-themes-dark.git", branch: CORE_VERSION
- gem 'openproject-local_avatars', git: "https://github.com/finnlabs/openproject-local_avatars", branch: CORE_VERSION
- gem 'openproject-webhooks', git: "https://github.com/finnlabs/openproject-webhooks", branch: CORE_VERSION
- gem 'openproject-github_integration', git: "https://github.com/finnlabs/openproject-github_integration", branch: CORE_VERSION
+ gem "openproject-themes-dark", path: "vendored-plugins/openproject-themes-dark"
+ gem "openproject-local_avatars", path: "vendored-plugins/openproject-local_avatars"
+ gem "openproject-webhooks", path: "vendored-plugins/openproject-webhooks"
+ gem "openproject-github_integration", path: "vendored-plugins/openproject-github_integration"
end
diff --git a/vendored-plugins/openproject-announcements/CHANGELOG.md b/vendored-plugins/openproject-announcements/CHANGELOG.md
new file mode 100644
index 0000000000..9cd7cfac0e
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/CHANGELOG.md
@@ -0,0 +1,38 @@
+# Changelog
+
+## 3.0.8
+
+* Fix path of asset that should be precompiled
+
+## 0.5.0
+
+* `#4024` Subpages have no unique page titles
+
+## 0.5.0.pre7
+
+* `#2163` [Accessibility] Link form elements to their label - announcment
+
+## 0.5.0.pre6
+
+* Adaptations for new icon font
+
+## 0.5.0.pre5
+
+* `#2378` Squashed old migrations
+
+## 0.5.0.pre4
+
+* `#2070` Adaptions after changing core asset locations
+
+## 0.5.0.pre3
+
+* Spec fixes
+
+## 0.5.0.pre2
+
+* `#1678` Date is not validated, wrong format produces internal error
+
+## 0.5.0.pre1
+
+* First Rails 3.2 version of this plugin
+* `#1469` Migration Rails3 Announcement
diff --git a/vendored-plugins/openproject-announcements/README.md b/vendored-plugins/openproject-announcements/README.md
new file mode 100644
index 0000000000..92de4e123a
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/README.md
@@ -0,0 +1,3 @@
+# OpenProject Announcements
+
+OpenProject Plugin for showing announcements on the login page
diff --git a/vendored-plugins/openproject-announcements/app/assets/stylesheets/announcements/announcements.css.erb b/vendored-plugins/openproject-announcements/app/assets/stylesheets/announcements/announcements.css.erb
new file mode 100644
index 0000000000..944d6e3538
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/assets/stylesheets/announcements/announcements.css.erb
@@ -0,0 +1,10 @@
+#announcement {
+ margin-left: auto;
+ margin-right: auto;
+ background-color: transparent;
+ margin-top: 3em;
+ padding-left: 27px;
+ padding-top: 2px;
+ width: 370px;
+ position: relative;
+}
diff --git a/vendored-plugins/openproject-announcements/app/controllers/announcements_controller.rb b/vendored-plugins/openproject-announcements/app/controllers/announcements_controller.rb
new file mode 100644
index 0000000000..07d4c784d7
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/controllers/announcements_controller.rb
@@ -0,0 +1,30 @@
+class AnnouncementsController < ApplicationController
+ layout 'admin'
+
+ before_filter :require_admin
+
+ def edit
+ @announcement = Announcement.only_one
+ end
+
+ def update
+ @announcement = Announcement.only_one
+ @announcement.attributes = announcement_params
+
+ if @announcement.save
+ flash[:notice] = l(:notice_successful_update)
+ end
+
+ render :action => 'edit'
+ end
+
+ private
+
+ def default_breadcrumb
+ l('announcements.announcement')
+ end
+
+ def announcement_params
+ params.require(:announcement).permit('text', 'show_until', 'active')
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/app/helpers/announcements_helper.rb b/vendored-plugins/openproject-announcements/app/helpers/announcements_helper.rb
new file mode 100644
index 0000000000..9f907065cc
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/helpers/announcements_helper.rb
@@ -0,0 +1,9 @@
+module AnnouncementsHelper
+ def notice_annoucement_active
+ if @announcement.active_and_current?
+ l(:'announcements.is_active')
+ else
+ l(:'announcements.is_inactive')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/app/models/announcement.rb b/vendored-plugins/openproject-announcements/app/models/announcement.rb
new file mode 100644
index 0000000000..20600f2be6
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/models/announcement.rb
@@ -0,0 +1,28 @@
+class Announcement < ActiveRecord::Base
+
+ scope :active, -> { where(active: true) }
+ scope :current, -> { where('show_until >= ?', Date.today) }
+
+ validates :show_until, :presence => true
+
+ def self.active_and_current
+ active.current.first
+ end
+
+ def self.only_one
+ a = first
+ a = create_default_announcement if a.nil?
+ a
+ end
+
+ def active_and_current?
+ active? && show_until && show_until >= Date.today
+ end
+
+ private
+ def self.create_default_announcement
+ Announcement.create :text => "Announcement",
+ :show_until => Date.today + 14.days,
+ :active => false
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/app/views/announcements/_show.html.erb b/vendored-plugins/openproject-announcements/app/views/announcements/_show.html.erb
new file mode 100644
index 0000000000..ed3d6f7159
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/views/announcements/_show.html.erb
@@ -0,0 +1,13 @@
+<% def active_announcement
+ @announcement ||= Announcement.active_and_current
+ end
+%>
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'announcements/announcements.css' %>
+<% end %>
+
+<% unless active_announcement.nil? %>
+
+ <%= active_announcement.text.html_safe %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-announcements/app/views/announcements/edit.html.erb b/vendored-plugins/openproject-announcements/app/views/announcements/edit.html.erb
new file mode 100644
index 0000000000..bfe22d90dc
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/app/views/announcements/edit.html.erb
@@ -0,0 +1,22 @@
+<% html_title l(:label_administration), l("announcements.announcement") %>
+
+<%= error_messages_for 'announcement' %>
+
+<%= toolbar title: "#{l('announcements.announcement')} (#{notice_annoucement_active})" %>
+
+<%= labelled_tabular_form_for @announcement,
+ :url => {:action => :update},
+ :html => {:method => :put} do |f|%>
+
+ <%= f.text_area :text, :cols => 80, :rows => 5, label: l('announcements.text') %>
+
+
+ <%= f.text_field :show_until, label: l("announcements.show_until") %>
+ <%= calendar_for("announcement_show_until") %>
+
+
+ <%= f.check_box :active, label: l('announcements.active') %>
+
+
+ <%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
+<% end %>
diff --git a/vendored-plugins/openproject-announcements/config/locales/de.yml b/vendored-plugins/openproject-announcements/config/locales/de.yml
new file mode 100644
index 0000000000..5e8ac3528d
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/config/locales/de.yml
@@ -0,0 +1,15 @@
+de:
+ label_show_until: Zeigen bis einschließlich
+
+ activerecord:
+ attributes:
+ announcement:
+ show_until: Zeigen bis einschließlich
+
+ announcements:
+ announcement: Ankündigung
+ is_active: derzeit angezeigt
+ is_inactive: derzeit nicht angezeigt
+ show_until: Zeigen bis einschließlich
+ active: Aktiv
+ text: Text
diff --git a/vendored-plugins/openproject-announcements/config/locales/en.yml b/vendored-plugins/openproject-announcements/config/locales/en.yml
new file mode 100644
index 0000000000..d2b24ac646
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/config/locales/en.yml
@@ -0,0 +1,15 @@
+en:
+ label_show_until: Show until
+
+ activerecord:
+ attributes:
+ announcement:
+ show_until: Display until
+
+ announcements:
+ announcement: Announcement
+ is_active: currently displayed
+ is_inactive: currently not displayed
+ show_until: Display until
+ active: Active
+ text: Text
diff --git a/vendored-plugins/openproject-announcements/config/routes.rb b/vendored-plugins/openproject-announcements/config/routes.rb
new file mode 100644
index 0000000000..8d5173fc09
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/config/routes.rb
@@ -0,0 +1,3 @@
+OpenProject::Application.routes.draw do
+ resource :announcements, :path => '/admin/announcement', :only => [:edit, :update]
+end
diff --git a/vendored-plugins/openproject-announcements/db/migrate/20121114100641_aggregated_announcements_migrations.rb b/vendored-plugins/openproject-announcements/db/migrate/20121114100641_aggregated_announcements_migrations.rb
new file mode 100644
index 0000000000..6a17b9c584
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/db/migrate/20121114100641_aggregated_announcements_migrations.rb
@@ -0,0 +1,29 @@
+require Rails.root.join("db","migrate","migration_utils","migration_squasher").to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in the MIGRATION_FILES
+class AggregatedAnnouncementsMigrations < ActiveRecord::Migration
+
+ MIGRATION_FILES = <<-MIGRATIONS
+ 001_create_announcements.rb
+ 20121114100640_index_on_announcements.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = "redmine_announcements"
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table :announcements do |t|
+ t.text :text
+ t.date :show_until
+ t.boolean :active, :default => false
+ t.timestamps
+ end
+ add_index :announcements, [:show_until, :active]
+ end
+ end
+
+ def down
+ drop_table :announcements
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/features/administration.feature b/vendored-plugins/openproject-announcements/features/administration.feature
new file mode 100644
index 0000000000..7fe0ddaacb
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/features/administration.feature
@@ -0,0 +1,17 @@
+Feature: When I am an admin
+ I want to enter announcements
+ To inform all system users of upcomming events
+
+ Scenario: Announcements are editable in the administrations area
+ Given I am admin
+ And I go to the admin page
+ Then I should see "Announcement"
+
+ Scenario: Editing the announcement
+ Given I am admin
+ When I go to the edit page of Announcement
+ And I enter "Time.now" into the announcement date field
+ And I enter "We will have a downtime" into the announcement text field
+ And I activate the announcement
+ And I click on "Save"
+ Then I should see "Successful update"
diff --git a/vendored-plugins/openproject-announcements/features/step_definitions/announcement_edit_steps.rb b/vendored-plugins/openproject-announcements/features/step_definitions/announcement_edit_steps.rb
new file mode 100644
index 0000000000..5f2137355a
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/features/step_definitions/announcement_edit_steps.rb
@@ -0,0 +1,20 @@
+When /^I enter (.+) into the announcement date field$/ do |time|
+ time = eval(time.gsub("\"", "")).strftime("%Y-%m-%d")
+
+ steps %Q{
+ When I fill in "announcement_show_until" with "#{time}"
+ }
+end
+
+When /^I enter (.+) into the announcement text field$/ do |text|
+ text = text.gsub("\"", '')
+ steps %Q{
+ When I fill in "announcement_text" with "#{text}"
+ }
+end
+
+When /^I activate the announcement$/ do
+ steps %Q{
+ When I check "announcement_active"
+ }
+end
diff --git a/vendored-plugins/openproject-announcements/features/step_definitions/announcement_general_steps.rb b/vendored-plugins/openproject-announcements/features/step_definitions/announcement_general_steps.rb
new file mode 100644
index 0000000000..b6a514aaf5
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/features/step_definitions/announcement_general_steps.rb
@@ -0,0 +1,5 @@
+Given /^there is an (active|inactive) announcement saying (.+)$/ do |status, text|
+ active = status == "active" ? true : false
+ text = text.gsub("\"","")
+ FactoryGirl.create(:announcement, :text => text, :active => active)
+end
diff --git a/vendored-plugins/openproject-announcements/features/support/paths.rb b/vendored-plugins/openproject-announcements/features/support/paths.rb
new file mode 100644
index 0000000000..e6adc0a651
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/features/support/paths.rb
@@ -0,0 +1,12 @@
+module AnnoucementNavigationHelpers
+ def path_to(page_name)
+ case page_name
+ when /the edit page of Announcement/
+ '/admin/announcement/edit'
+ else
+ super
+ end
+ end
+end
+
+World(AnnoucementNavigationHelpers)
diff --git a/vendored-plugins/openproject-announcements/features/user_notification.feature b/vendored-plugins/openproject-announcements/features/user_notification.feature
new file mode 100644
index 0000000000..01d5f01215
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/features/user_notification.feature
@@ -0,0 +1,13 @@
+Feature: As a system user
+ I want to be informed about announcements e.g. downtimes
+ So that I know what is happening with the system
+
+ Scenario: Active announcements are displayed on the login screen
+ Given there is an active announcement saying "Downtime"
+ When I go to the login page
+ Then I should see "Downtime"
+
+ Scenario: Inactive annoucements are not displayed on the login screen
+ Given there is an inactive announcement saying "Downtime"
+ When I go to the login page
+ Then I should not see "Donwtime"
diff --git a/vendored-plugins/openproject-announcements/lib/open_project/announcements.rb b/vendored-plugins/openproject-announcements/lib/open_project/announcements.rb
new file mode 100644
index 0000000000..8f345eba32
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/lib/open_project/announcements.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module Announcements
+ require "open_project/announcements/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/lib/open_project/announcements/engine.rb b/vendored-plugins/openproject-announcements/lib/open_project/announcements/engine.rb
new file mode 100644
index 0000000000..567a22be7c
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/lib/open_project/announcements/engine.rb
@@ -0,0 +1,69 @@
+module OpenProject::Announcements
+ class Engine < ::Rails::Engine
+ engine_name :openproject_announcements
+
+ config.autoload_paths += Dir["#{config.root}/lib/"]
+
+ spec = Bundler.environment.specs['openproject-announcements'][0]
+ initializer 'announcements.register_plugin' do
+ Redmine::Plugin.register :openproject_announcements do
+ name 'OpenProject Announcements'
+ author ((spec.authors.kind_of? Array) ? spec.authors[0] : spec.authors)
+ author_url spec.homepage
+ description spec.description
+ version spec.version
+ url 'https://www.openproject.org/projects/announcement'
+
+ requires_openproject ">= 4.0.0"
+
+ menu :admin_menu,
+ :announcements,
+ {:controller => 'announcements', :action => 'edit'},
+ :caption => 'Announcement',
+ :html => {:class => 'icon2 icon-news'}
+ end
+ end
+
+ initializer 'announcements.precompile_assets' do
+ Rails.application.config.assets.precompile += %w(announcements/announcements.css)
+ end
+
+ # adds our factories to factory girl's load path
+ initializer "announcements.register_factories", :after => "factory_girl.set_factory_paths" do |app|
+ if defined?(FactoryGirl)
+ FactoryGirl.definition_file_paths << File.expand_path(self.root.to_s + '/spec/factories')
+ end
+ end
+
+ initializer 'announcements.register_test_paths' do |app|
+ app.config.plugins_to_test_paths << self.root
+ end
+
+ initializer 'announcements.append_migrations' do |app|
+ unless app.root.to_s.match root.to_s
+ config.paths["db/migrate"].expanded.each do |expanded_path|
+ app.config.paths["db/migrate"] << expanded_path
+ end
+ end
+ end
+
+ config.before_configuration do |app|
+ # This is required for the routes to be loaded first
+ # as the routes should be prepended so they take precedence over the core.
+ app.config.paths['config/routes.rb'].unshift File.join(File.dirname(__FILE__),
+ "..", "..", "..", "config", "routes.rb")
+ end
+
+ initializer "announcements.remove_duplicate_routes", :after => "add_routing_paths" do |app|
+ # removes duplicate entry from app.routes_reloader
+ # As we prepend the plugin's routes to the load_path up front and rails
+ # adds all engines' config/routes.rb later, we have double loaded the routes
+ # This is not harmful as such but leads to duplicate routes which decreases performance
+ app.routes_reloader.paths.uniq!
+ end
+
+ config.to_prepare do
+ require_dependency 'open_project/announcements/hooks'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/lib/open_project/announcements/hooks.rb b/vendored-plugins/openproject-announcements/lib/open_project/announcements/hooks.rb
new file mode 100644
index 0000000000..edd443a8a9
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/lib/open_project/announcements/hooks.rb
@@ -0,0 +1,5 @@
+module Announcements
+ class Hooks < Redmine::Hook::ViewListener
+ render_on :view_account_login_bottom, :partial => "announcements/show"
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/lib/open_project/announcements/version.rb b/vendored-plugins/openproject-announcements/lib/open_project/announcements/version.rb
new file mode 100644
index 0000000000..3a7e9d45fa
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/lib/open_project/announcements/version.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module Announcements
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/lib/openproject-announcements.rb b/vendored-plugins/openproject-announcements/lib/openproject-announcements.rb
new file mode 100644
index 0000000000..0c668e773e
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/lib/openproject-announcements.rb
@@ -0,0 +1 @@
+require 'open_project/announcements'
diff --git a/vendored-plugins/openproject-announcements/openproject-announcements.gemspec b/vendored-plugins/openproject-announcements/openproject-announcements.gemspec
new file mode 100644
index 0000000000..534179da07
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/openproject-announcements.gemspec
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/announcements/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-announcements"
+ s.version = OpenProject::Announcements::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/announcement/"
+ s.summary = 'OpenProject Plugin for showing announcements on the login page'
+ s.description = s.summary
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + %w(CHANGELOG.md README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+
+ s.add_development_dependency 'rspec-rails'
+ s.add_development_dependency 'cucumber-rails'
+ s.add_development_dependency 'database_cleaner'
+end
diff --git a/vendored-plugins/openproject-announcements/spec/controllers/announcements_controller_spec.rb b/vendored-plugins/openproject-announcements/spec/controllers/announcements_controller_spec.rb
new file mode 100644
index 0000000000..4c150ee267
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/spec/controllers/announcements_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe AnnouncementsController, :type => :controller do
+ before(:each) do
+ allow(@controller).to receive(:check_if_login_required)
+ expect(@controller).to receive(:require_admin)
+
+ @announcement = mock_model Announcement
+ allow(Announcement).to receive(:only_one).and_return(@announcement)
+ disable_flash_sweep
+ end
+
+ describe '#get' do
+ before :each do
+
+ end
+
+ describe '#edit' do
+ before :each do
+ @params = {}
+ end
+
+ describe "SUCCESS" do
+ describe "html" do
+ before :each do
+ get :edit, @params
+ end
+
+ it{expect(assigns(:announcement)).to eql @announcement}
+ it{expect(response).to be_success}
+ end
+ end
+ end
+ end
+
+ describe '#put' do
+ before :each do
+ end
+
+ describe '#update' do
+ before :each do
+ @params = {"announcement" => {"until_date" => "2011-01-11",
+ "text" => "announcement!!!",
+ "active" => "1"}}
+ end
+
+ describe "SUCCESS" do
+ before :each do
+ expect(@announcement).to receive(:attributes=)
+ expect(@announcement).to receive(:save).and_return(true)
+ end
+
+ describe "html" do
+ before :each do
+ put :update, @params
+ end
+
+ it{expect(assigns(:announcement)).to eql @announcement}
+ it{expect(response).to render_template 'edit'}
+ it{expect(flash[:notice]).to eql I18n.t(:notice_successful_update)}
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/spec/factories/announcement_factory.rb b/vendored-plugins/openproject-announcements/spec/factories/announcement_factory.rb
new file mode 100644
index 0000000000..83dde94f25
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/spec/factories/announcement_factory.rb
@@ -0,0 +1,15 @@
+FactoryGirl.define do
+ factory :announcement do
+ text "Announcement text"
+ show_until Date.today + 14.days
+ active true
+
+ factory :active_announcement do
+ active true
+ end
+
+ factory :inactive_announcement do
+ active false
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-announcements/spec/models/announcement_spec.rb b/vendored-plugins/openproject-announcements/spec/models/announcement_spec.rb
new file mode 100644
index 0000000000..0ec5237622
--- /dev/null
+++ b/vendored-plugins/openproject-announcements/spec/models/announcement_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Announcement, :type => :model do
+ it {is_expected.to respond_to :text}
+ it {is_expected.to respond_to :text=}
+ it {is_expected.to respond_to :show_until}
+ it {is_expected.to respond_to :show_until=}
+ it {is_expected.to respond_to :active?}
+ it {is_expected.to respond_to :active=}
+
+ describe "class methods" do
+ before :each do
+
+ end
+
+ describe '#only_one' do
+ before :each do
+
+ end
+
+ context "WHEN no announcement exists" do
+ before :each do
+
+ end
+
+ it {expect(Announcement.only_one.text).to eql "Announcement"}
+ it {expect(Announcement.only_one.show_until).to eql(Date.today + 14.days)}
+ it {expect(Announcement.only_one.active).to eql false}
+
+ end
+
+ context "WHEN an announcement exists" do
+ before :each do
+ @announcement = FactoryGirl.create(:announcement)
+ end
+
+ it{expect(Announcement.only_one).to eql @announcement}
+ end
+ end
+
+ describe '#active_and_current' do
+ describe "WHEN no announcement is active" do
+ before :each do
+ FactoryGirl.create(:inactive_announcement)
+ end
+
+ it{ expect(Announcement.active_and_current).to be_nil }
+ end
+
+ describe "WHEN the one announcement is active and today is before show_until" do
+ before :each do
+ @announcement = FactoryGirl.create(:active_announcement,
+ :show_until => Date.today + 14.days)
+ end
+
+ it{ expect(Announcement.active_and_current).to eql @announcement }
+ end
+
+ describe "WHEN the one announcement is active and today is after show_until" do
+ before :each do
+ FactoryGirl.create(:active_announcement,
+ :show_until => Date.today - 14.days)
+ end
+
+ it{ expect(Announcement.active_and_current).to be_nil }
+ end
+
+ describe "WHEN the one announcement is active and today equals show_until" do
+ before :each do
+ @announcement = FactoryGirl.create(:active_announcement,
+ :show_until => Date.today)
+ end
+
+ it{ expect(Announcement.active_and_current).to eql @announcement }
+ end
+ end
+
+ describe "instance methods" do
+ describe '#active_and_current?' do
+ describe "WHEN the announcement is not active" do
+ before :each do
+ @announcement = FactoryGirl.build(:inactive_announcement)
+ end
+
+ it{ expect(@announcement.active_and_current?).to be_falsey }
+ end
+
+ describe "WHEN the announcement is active and today is before show_until" do
+ before :each do
+ @announcement = FactoryGirl.build(:active_announcement,
+ :show_until => Date.today + 14.days)
+ end
+
+ it{ expect(@announcement.active_and_current?).to be_truthy }
+ end
+
+ describe "WHEN the announcement is active and today is after show_until" do
+ before :each do
+ @announcement = FactoryGirl.build(:active_announcement,
+ :show_until => Date.today - 14.days)
+ end
+
+ it{ expect(@announcement.active_and_current?).to be_falsey }
+ end
+
+ describe "WHEN the announcement is active and today equals show_until" do
+ before :each do
+ @announcement = FactoryGirl.build(:active_announcement,
+ :show_until => Date.today)
+ end
+
+ it{ expect(@announcement.active_and_current?).to be_truthy }
+ end
+ end
+
+
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/.hound.yml b/vendored-plugins/openproject-auth_plugins/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-auth_plugins/.rubocop.yml b/vendored-plugins/openproject-auth_plugins/.rubocop.yml
new file mode 100644
index 0000000000..a22df7c695
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - *.gemspec
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
\ No newline at end of file
diff --git a/vendored-plugins/openproject-auth_plugins/README.md b/vendored-plugins/openproject-auth_plugins/README.md
new file mode 100644
index 0000000000..2dfcb2db7c
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/README.md
@@ -0,0 +1,107 @@
+# OpenProject AuthPlugins Plugin
+
+Adds support for easy integration of OmniAuth strategy providers as a means to authenticate users in OpenProject.
+
+## Usage
+
+ gem 'openproject-auth_plugins', :git => 'git@github.com:finnlabs/openproject-auth_plugins', :branch => 'stable'
+
+You can use this plugin to make an authentication plugin out of an ordinary OpenProject plugin.
+The first step is to generate a new plugin.
+Once you have done that it only takes a few additions to make it an authentication plugin.
+Find your Engine class in `engine.rb`, let it extend `OpenProject::Plugin::AuthPlugin` and register the providers you want to use.
+
+Here's an example of how that might look:
+
+```ruby
+module OpenProject::SomeAuthPlugin
+ class Engine < ::Rails::Engine
+ engine_name :openproject_some_auth_plugin
+
+ include OpenProject::Plugins::ActsAsOpEngine
+ extend OpenProject::Plugins::AuthPlugin # just add this ...
+
+ register 'openproject-some_auth_plugin',
+ author_url: 'http://my.site',
+ requires_openproject: '>= 3.1.0pre1'
+
+ assets %w(
+ some_auth_plugin/some_provider.png
+ )
+
+ # to get #register_auth_providers:
+ register_auth_providers do
+ strategy :some_strategy do
+ [
+ {
+ name: 'some_provider',
+ host: 'foo.bar.baz',
+ port: 999,
+ #, ... more provider options
+ icon: 'some_auth_plugin/some_provider.png'
+ },
+ {
+ name: 'another_provider',
+ host: 'foobar.biz',
+ port: '692',
+ #, ... more provider options
+ display_name: 'Provider 2'
+ # ... provide custom attribute mapping
+ openproject_attribute_map: Proc.new {|auth| { login: auth[:info][:uid] } }
+ }
+ ]
+ end
+
+ strategy :another_strategy do
+ [{name: 'yet_another_provider'}]
+ end
+ end
+ end
+end
+```
+
+Register each OmniAuth strategy by calling `strategy` with the strategy's name and returning the options for the providers using that strategy in the passed block. Provider options must at the very least contain a `name` that has to be unique among all strategies' providers. The rest depends on the used strategy.
+
+**Additional provider attribute `icon`**
+
+As you can see in the first registered provider you can also give a new option called `icon`.
+Using this option you can define which icon is to be rendered for the given provider.
+In the example our own plugin provides the icon. In the plugin's directory it has to be placed under `app/assets/images/some_auth_plugin/some_provider.png`.
+
+**Additional provider attribute `display_name`**
+
+Another extra attribute shown is `display_name`. While `name` is used to identify the provider in URLs `display_name` is what is shown to the user.
+
+**Additional provider attribute `openproject_attribute_map`**
+
+To provide a custom user attribute mapping for this strategy, you may optionally specify a block that returns an attribute mapping hash. In the examplary strategy *another_provider*, the OpenProject attribute `:login` is overridden reflect the attribute `:uid` from the strategy.
+
+The block is called with the [OmniAuth AuthHash object](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema). You can use the `:extra` key to access the raw attributes as returned from the authentication schema.
+
+## OpenProject Integration
+
+For each registered provider a button will be added to the OpenProject login screen as shown in the following example:
+
+![OpenProject Login Screen](../screenshots/login_screen_en.png?raw=true "Login screen showing buttons for 6 providers.")
+
+In this example an icon has only been defined for 'Google'.
+All other providers just show a default icon.
+
+### Runtime Changes
+
+All used strategies have to be known at the start of the application.
+Providers, however, can change arbitrarily at runtime.
+The block passed to `#strategy` is called each time an authentication request is made.
+
+## Repository
+
+This repository contains two main branches:
+
+* `dev`: The main development branch. We try to keep it stable in the sense of all tests are passing, but we don't recommend it for production systems.
+* `stable`: Contains the latest stable release that we recommend for production use. Use this if you always want the latest version of this plugin.
+
+## License
+
+Copyright (C) 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See [doc/COPYRIGHT.md](doc/COPYRIGHT.md) for details.
diff --git a/vendored-plugins/openproject-auth_plugins/app/views/hooks/login/_providers.html.erb b/vendored-plugins/openproject-auth_plugins/app/views/hooks/login/_providers.html.erb
new file mode 100644
index 0000000000..73ee822a5d
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/app/views/hooks/login/_providers.html.erb
@@ -0,0 +1,53 @@
+<%#-- copyright
+OpenProject is a project management system.
+Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% OpenProject::Plugins::AuthPlugin.providers.each do |pro| %>
+ <%
+ opts = {
+ :controller => '/auth',
+ :action => pro[:name]
+ }
+ if params['back_url']
+ opts[:origin] = params['back_url']
+ end
+ %>
+
+
+ <%= pro[:display_name] || pro[:name] %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-auth_plugins/doc/CHANGELOG.md b/vendored-plugins/openproject-auth_plugins/doc/CHANGELOG.md
new file mode 100644
index 0000000000..a486701cc6
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/doc/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Changelog
+
+## 0.1.2
+
+* use English screenshot in README
+* show license in README
+* fixed project link in gemspec
+
+## 0.1.1
+
+* updated readme to advise using the stable branch of the plugin
+* specified omniauth version in gemspec
+
+## 0.1.0
+
+* first release
diff --git a/vendored-plugins/openproject-auth_plugins/doc/COPYRIGHT.rdoc b/vendored-plugins/openproject-auth_plugins/doc/COPYRIGHT.rdoc
new file mode 100644
index 0000000000..6ecaaee653
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/doc/COPYRIGHT.rdoc
@@ -0,0 +1,59 @@
+OpenProject is a project management system.
+
+Copyright (C)2012-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+---
+
+OpenProject is a derivative work based on ChiliProject, whose Copyright follows.
+
+ChiliProject is a project management system.
+
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+---
+
+ChiliProject is a derivative work based on Redmine, whose Copyright follows.
+
+Redmine - project management software
+Copyright (C) 2006-2013 Jean-Philippe Lang
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_builder.rb b/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_builder.rb
new file mode 100644
index 0000000000..1679a6e686
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_builder.rb
@@ -0,0 +1,37 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OmniAuth
+ class FlexibleBuilder < Builder
+ def use(middleware, *args, &block)
+ middleware.extend FlexibleStrategyClass
+ super(middleware, *args, &block)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_strategy.rb b/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_strategy.rb
new file mode 100644
index 0000000000..3b853982cb
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/omniauth/flexible_strategy.rb
@@ -0,0 +1,100 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/plugins/auth_plugin'
+
+module OmniAuth
+ module FlexibleStrategyClass
+ def new(app, *args, &block)
+ super(app, *args, &block).tap do |strategy|
+ strategy.extend FlexibleStrategy
+ end
+ end
+ end
+
+ module FlexibleStrategy
+ def on_auth_path?
+ possible_auth_path? && (match_provider! || false) && super
+ end
+
+ ##
+ # Tries to match the request path of the current request with one of the registered providers.
+ # If a match is found the strategy is intialised with that provider to handle the request.
+ def match_provider!
+ return false unless providers
+
+ @provider = providers.find do |p|
+ (current_path =~ /#{path_for_provider(p.to_hash[:name])}/) == 0
+ end
+
+ if @provider
+ options.merge! provider.to_hash
+ end
+
+ @provider
+ end
+
+ def omniauth_hash_to_user_attributes(auth)
+ if options.key?(:openproject_attribute_map)
+ options[:openproject_attribute_map].call(auth)
+ else
+ {}
+ end
+ end
+
+ def path_for_provider(name)
+ "#{path_prefix}/#{name}"
+ end
+
+ ##
+ # Returns true if the current path could be an authentication request,
+ # false otherwise (e.g. for resources).
+ def possible_auth_path?
+ current_path =~ /\A#{path_prefix}/
+ end
+
+ def providers
+ @providers ||= OpenProject::Plugins::AuthPlugin.providers_for(self.class)
+ end
+
+ def provider
+ @provider
+ end
+
+ def providers=(providers)
+ @providers = providers
+ end
+
+ def dup
+ super.tap do |s|
+ s.extend FlexibleStrategy
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins.rb b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins.rb
new file mode 100644
index 0000000000..3ddb40b0ef
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins.rb
@@ -0,0 +1,37 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module AuthPlugins
+ require 'open_project/plugins/auth_plugin'
+ require 'omniauth/flexible_builder'
+ require 'omniauth/flexible_strategy'
+ require 'open_project/auth_plugins/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/engine.rb b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/engine.rb
new file mode 100644
index 0000000000..5a3ea8a087
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/engine.rb
@@ -0,0 +1,46 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/plugins'
+
+module OpenProject::AuthPlugins
+ class Engine < ::Rails::Engine
+ engine_name :openproject_auth_plugins
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-auth_plugins',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 3.1.0pre1'
+
+ initializer 'auth_plugins.register_hooks' do
+ require 'open_project/auth_plugins/hooks'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/hooks.rb b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/hooks.rb
new file mode 100644
index 0000000000..9cd6797040
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/hooks.rb
@@ -0,0 +1,34 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::AuthPlugins
+ class Hooks < Redmine::Hook::ViewListener
+ render_on :view_account_login_auth_provider, partial: 'hooks/login/providers'
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/version.rb b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/version.rb
new file mode 100644
index 0000000000..244efdac1b
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/open_project/auth_plugins/version.rb
@@ -0,0 +1,34 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module AuthPlugins
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/open_project/plugins/auth_plugin.rb b/vendored-plugins/openproject-auth_plugins/lib/open_project/plugins/auth_plugin.rb
new file mode 100644
index 0000000000..f2e53e4ffb
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/open_project/plugins/auth_plugin.rb
@@ -0,0 +1,86 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Plugins
+ module AuthPlugin
+ def register_auth_providers(&build_providers)
+ initializer "#{engine_name}.middleware" do |app|
+ builder = ProviderBuilder.new
+ builder.instance_eval(&build_providers)
+
+ app.config.middleware.use OmniAuth::FlexibleBuilder do
+ builder.new_strategies.each do |strategy|
+ provider strategy
+ end
+ end
+ end
+ end
+
+ def self.strategies
+ @strategies ||= {}
+ end
+
+ def self.providers_for(strategy)
+ strategies[strategy_key(strategy)].map(&:call).flatten.map(&:to_hash)
+ end
+
+ def self.providers
+ strategies.values.flatten.map(&:call).flatten.map(&:to_hash)
+ end
+
+ def self.strategy_key(strategy)
+ return strategy if strategy.is_a? Symbol
+
+ name = strategy.name.demodulize
+ camelization = OmniAuth.config.camelizations.select do |_k, v|
+ v == name
+ end.take(1).map do |k, _v|
+ k
+ end.first
+
+ [camelization, name].compact.first.underscore.to_sym
+ end
+ end
+
+ class ProviderBuilder
+ def strategy(strategy, &providers)
+ key = AuthPlugin.strategy_key(strategy)
+ if AuthPlugin.strategies.include? key
+ AuthPlugin.strategies[key] << providers
+ else
+ AuthPlugin.strategies[key] = [providers]
+ new_strategies << strategy
+ end
+ end
+
+ def new_strategies
+ @new_strategies ||= []
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/lib/openproject-auth_plugins.rb b/vendored-plugins/openproject-auth_plugins/lib/openproject-auth_plugins.rb
new file mode 100644
index 0000000000..5a3f70ab2f
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/lib/openproject-auth_plugins.rb
@@ -0,0 +1,30 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/auth_plugins'
diff --git a/vendored-plugins/openproject-auth_plugins/openproject-auth_plugins.gemspec b/vendored-plugins/openproject-auth_plugins/openproject-auth_plugins.gemspec
new file mode 100644
index 0000000000..a5e420b3b0
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/openproject-auth_plugins.gemspec
@@ -0,0 +1,22 @@
+# encoding: UTF-8
+$:.push File.expand_path('../lib', __FILE__)
+
+require 'open_project/auth_plugins/version'
+
+Gem::Specification.new do |s|
+ s.name = 'openproject-auth_plugins'
+ s.version = OpenProject::AuthPlugins::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/auth-plugins'
+ s.summary = 'OpenProject Auth Plugins'
+ s.description = 'Integration of OmniAuth strategy providers for authentication in Openproject.'
+ s.license = 'GPLv3'
+
+ s.files = Dir['{app,config,db,lib}/**/*'] + %w(doc/CHANGELOG.md README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency 'omniauth', '~> 1.0'
+
+ s.add_development_dependency 'rspec', '~> 2.14'
+end
diff --git a/vendored-plugins/openproject-auth_plugins/spec/requests/auth_plugins.rb b/vendored-plugins/openproject-auth_plugins/spec/requests/auth_plugins.rb
new file mode 100644
index 0000000000..14b55688e4
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/spec/requests/auth_plugins.rb
@@ -0,0 +1,102 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'open_project/auth_plugins'
+
+describe OpenProject::Plugins::AuthPlugin do
+
+ class MockEngine
+ extend OpenProject::Plugins::AuthPlugin
+ end
+
+ let(:strategies) { {} }
+ let(:providers_a) do
+ lambda { [{ name: 'a1' }, { name: 'a2' }] }
+ end
+ let(:providers_b) do
+ lambda { [{ name: 'b1' }] }
+ end
+ let(:providers_c) do
+ lambda { [{ name: 'c1' }] }
+ end
+
+ let(:middlewares) { [] }
+
+ before do
+ app = Object.new
+ omniauth_builder = Object.new
+
+ allow(omniauth_builder).to receive(:provider) { |strategy|
+ middlewares << strategy
+ }
+
+ allow(app).to receive_message_chain(:config, :middleware, :use) { |_mw, &block|
+ omniauth_builder.instance_eval(&block)
+ }
+
+ allow(OpenProject::Plugins::AuthPlugin).to receive(:strategies).and_return(strategies)
+ allow(MockEngine).to receive(:engine_name).and_return('foobar')
+ allow(MockEngine).to receive(:initializer) { |_, &block| app.instance_eval(&block) }
+ end
+
+ describe 'ProviderBuilder' do
+ before do
+ pa = providers_a.call
+ pb = providers_b.call
+ pc = providers_c.call
+
+ Class.new(MockEngine) do
+ register_auth_providers do
+ strategy :strategy_a do; pa; end
+ strategy :strategy_b do; pb; end
+ end
+ end
+
+ Class.new(MockEngine) do
+ register_auth_providers do
+ strategy :strategy_a do; pc; end
+ end
+ end
+ end
+
+ it 'should register all strategies' do
+ expect(strategies.keys.to_a).to eq [:strategy_a, :strategy_b]
+ end
+
+ it 'should register register each strategy (i.e. middleware) only once' do
+ expect(middlewares.size).to eq 2
+ expect(middlewares).to eq [:strategy_a, :strategy_b]
+ end
+
+ it 'should associate the correct providers with their respective strategies' do
+ expect(OpenProject::Plugins::AuthPlugin.providers_for(:strategy_a)).to eq [providers_a.call, providers_c.call].flatten
+ expect(OpenProject::Plugins::AuthPlugin.providers_for(:strategy_b)).to eq providers_b.call
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/spec/requests/flexible_strategy_spec.rb b/vendored-plugins/openproject-auth_plugins/spec/requests/flexible_strategy_spec.rb
new file mode 100644
index 0000000000..41d9bf9fe7
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/spec/requests/flexible_strategy_spec.rb
@@ -0,0 +1,135 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe OmniAuth::FlexibleStrategy do
+ class MockStrategy
+ include OmniAuth::Strategy
+ include OmniAuth::FlexibleStrategy
+
+ def request_phase
+ call_app!
+ end
+ end
+
+ def env_for(url, opts = {})
+ Rack::MockRequest.env_for(url, opts).tap do |env|
+ env['rack.session'] = {}
+ end
+ end
+
+ let(:app) { ->(env) { [200, env, 'ok'] } }
+ let(:middleware) { MockStrategy.new(app) }
+ let(:provider_a) { { name: 'provider_a', identifier: 'a' } }
+ let(:provider_b) { { name: 'provider_b', identifier: 'b' } }
+
+ before do
+ allow(OpenProject::Plugins::AuthPlugin).to receive(:providers_for).with(MockStrategy) {
+ [provider_a, provider_b]
+ }
+ end
+
+ describe 'request call' do
+ it 'should match the registered providers' do
+ [provider_a, provider_b].each do |pro|
+ code, env = middleware.call env_for("http://www.example.com/auth/#{pro[:name]}")
+ strategy = env['omniauth.strategy']
+
+ # check that the correct provider has been initialised
+ expect(strategy.options.identifier).to eq pro[:identifier]
+ end
+ end
+
+ it 'should not match other paths' do
+ code, env = middleware.call env_for('http://www.example.com/auth/other_provider')
+
+ expect(env).not_to include 'omniauth.strategy' # no hit
+ end
+ end
+
+ describe 'callback call' do
+ before do
+ allow_any_instance_of(MockStrategy).to receive(:callback_phase).and_return(['hit'])
+ end
+
+ it 'should match the registered providers' do
+ [provider_a, provider_b].each do |pro|
+ code, _ = middleware.call env_for("http://www.example.com/auth/#{pro[:name]}/callback")
+
+ expect(code).to eq 'hit'
+ end
+ end
+
+ it 'should not match other paths' do
+ code, env = middleware.call env_for('http://www.example.com/auth/other_provider/callback')
+
+ expect(code).to eq 200
+ expect(env).not_to include 'omniauth.strategy' # no hit
+ end
+ end
+
+ describe 'calling strategies' do
+ let(:provider_with_mapping) do
+ {
+ name: 'provider_with_mapping',
+ openproject_attribute_map: Proc.new do |auth|
+ { uid: auth[:info][:myUsername], mail: auth[:extra][:raw_info][:myMail] }
+ end
+ }
+ end
+ let(:auth_hash) do
+ {
+ info: { myUsername: 'foo', myFullName: 'Foo Bar' },
+ extra: { raw_info: { myMail: 'foo@example.com' } }
+ }
+ end
+
+ before do
+ middleware.providers = [provider_a, provider_with_mapping]
+ end
+
+ context 'with a mapping set' do
+ it 'returns an attribute hash' do
+ middleware.call env_for('http://www.example.com/auth/provider_with_mapping')
+
+ attribute_map = middleware.omniauth_hash_to_user_attributes(auth_hash)
+ expect(attribute_map).to eq(uid: 'foo', mail: 'foo@example.com')
+ end
+ end
+
+ context 'without a mapping set' do
+ it 'returns an empty hash' do
+ middleware.call env_for('http://www.example.com/auth/provider_a')
+
+ attribute_map = middleware.omniauth_hash_to_user_attributes(auth_hash)
+ expect(attribute_map).to eq({})
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-auth_plugins/spec/views/hooks/login/_providers.html.erb_spec.rb b/vendored-plugins/openproject-auth_plugins/spec/views/hooks/login/_providers.html.erb_spec.rb
new file mode 100644
index 0000000000..60ea99e042
--- /dev/null
+++ b/vendored-plugins/openproject-auth_plugins/spec/views/hooks/login/_providers.html.erb_spec.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe 'rendering the login buttons for all providers' do
+ let(:providers) do
+ [
+ { name: 'mock_auth' },
+ { name: 'test_auth', display_name: 'Test' },
+ { name: 'foob_auth', icon: 'foobar.png' }
+ ]
+ end
+
+ before do
+ allow(OpenProject::Plugins::AuthPlugin).to receive(:providers).and_return(providers)
+
+ render partial: 'hooks/login/providers', handlers: [:erb], formats: [:html]
+ end
+
+ it 'should show the mock_auth button with the name as its label' do
+ expect(rendered).to match /#{providers[0][:name]}/
+ end
+
+ it 'should show the test_auth button with the given display_name as its label' do
+ expect(rendered).to match /#{providers[1][:display_name]}/
+ end
+
+ it 'should render a custom icon if defined' do
+ expect(rendered).to match /#{providers[2][:icon]}/
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/.gitignore b/vendored-plugins/openproject-backlogs/.gitignore
new file mode 100644
index 0000000000..cbf33a3a24
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/.gitignore
@@ -0,0 +1,10 @@
+# RubyMine files
+.idea/
+
+# Ignore any accidental remnants from gh-pages branch
+_site
+_posts
+assets/styles
+graph.dot
+*.swp
+node_modules
diff --git a/vendored-plugins/openproject-backlogs/.hound.yml b/vendored-plugins/openproject-backlogs/.hound.yml
new file mode 100644
index 0000000000..b7945ab908
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
diff --git a/vendored-plugins/openproject-backlogs/.rubocop.yml b/vendored-plugins/openproject-backlogs/.rubocop.yml
new file mode 100644
index 0000000000..2813c933b9
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/.rubocop.yml
@@ -0,0 +1,274 @@
+AllCops:
+ Exclude:
+ - "*.gemspec"
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+BlockDelimiters:
+ Enabled: true
+ EnforcedStyle: semantic
+ IgnoredMethods:
+ - default_scope
+ - lambda
+ - proc
+ - it
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
diff --git a/vendored-plugins/openproject-backlogs/README.md b/vendored-plugins/openproject-backlogs/README.md
new file mode 100644
index 0000000000..3b52154e5b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/README.md
@@ -0,0 +1,114 @@
+OpenProject Backlogs Plugin
+===========================
+
+This Plugin adds features, that enable agile teams to work efficiently with
+OpenProject in Scrum projects.
+
+Find a more detailed description on [OpenProject.org](https://www.openproject.org/projects/openproject/wiki/Agile_teams).
+
+Together with the plugin [OpenProject PDF Export](https://www.openproject.org/projects/pdf-export), story cards can be exported as printable PDF documents.
+
+Requirements
+------------
+
+The OpenProject Backlogs plug-in requires the [OpenProject Core](https://github.com/opf/openproject/) in
+version greater or equal to *3.0.0*.
+
+Tests for this plugin require `pdf-inspector`, so just add the following line to
+OpenProject's `Gemfile.plugins`:
+
+`gem "pdf-inspector", "~>1.0.0", :group => :test`
+
+
+Installation
+------------
+
+OpenProject Backlogs depends on OpenProject PDF export Plugin. Thus, if you haven't done
+it already, add the following lines to the `Gemfile.plugins` to your OpenProject installation (if you use a different OpenProject version than OpenProject 4.1, adapt :branch => "stable/4.1" to your OpenProject version):
+
+`gem "openproject-pdf_export", git: "https://github.com/finnlabs/openproject-pdf_export.git", :branch => "stable/4.1"`
+
+For OpenProject Backlogs itself you need to add the following line to the
+
+`Gemfile.plugins` of OpenProject (if you use a different OpenProject version than OpenProject 4.1, adapt :branch => "stable/4.1" to your OpenProject version):
+
+
+`gem "openproject-backlogs", git: "https://github.com/finnlabs/openproject-backlogs.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-backlogs", git: "https://github.com/finnlabs/openproject-backlogs.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this leaves plugin data in the database. Currently, we do not
+support full uninstall of the plugin.
+
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/plugin-backlogs
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/openproject-backlogs`
+
+
+Credits
+-------
+
+We thank the original maintainers and developers of [Redmine
+Backlogs](http://www.redminebacklogs.net/) as well as
+[Chiliproject Backlogs](https://github.com/finnlabs/chiliproject_backlogs) for
+their immense work on this plugin. OpenProject Backlogs would not have been
+possible without their original contribution. Those contributors are:
+
+* Marnen Laibow-Koser
+* Sandro Munda
+* Emiliano Heyns (friflaj)
+* Maxime Guilbot
+* Andrew Vit
+* Joakim Kolsjö
+* ibussieres
+* Daniel Passos
+* Jason Vasquez
+* jpic
+* Mark Maglana
+* Joe Heck
+* Nate Lowrie
+
+Additionally, we would like to thank
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorhip
+
+Licence
+-------
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 Emiliano Heyns (friflaj)
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/modal_close.png b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/modal_close.png
new file mode 100644
index 0000000000..7d52c7db70
Binary files /dev/null and b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/modal_close.png differ
diff --git a/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_background.png b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_background.png
new file mode 100644
index 0000000000..76efdebda1
Binary files /dev/null and b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_background.png differ
diff --git a/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_close.gif b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_close.gif
new file mode 100644
index 0000000000..4554ce9eb1
Binary files /dev/null and b/vendored-plugins/openproject-backlogs/app/assets/images/backlogs/livepipe-ui/window_close.gif differ
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlog.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlog.js
new file mode 100644
index 0000000000..ef446364ad
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlog.js
@@ -0,0 +1,186 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/******************************************
+ BACKLOG
+ A backlog is a visual representation of
+ a sprint and its stories. It is not a
+ sprint. Imagine it this way: A sprint is
+ a start and end date, and a set of
+ objectives. A backlog is something you
+ would draw up on the board or a spread-
+ sheet (or in Redmine Backlogs!) to
+ visualize the sprint.
+******************************************/
+
+RB.Backlog = (function ($) {
+ return RB.Object.create({
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+
+ // Make the list sortable
+ this.getList().sortable({
+ connectWith: '.stories',
+ dropOnEmpty: true,
+ start: this.dragStart,
+ stop: this.dragStop,
+ update: this.dragComplete,
+ receive: this.dragChanged,
+ remove: this.dragChanged,
+ containment: $('#backlogs_container'),
+ scroll: true,
+ helper: function(event, ui){
+ var $clone = $(ui).clone();
+ $clone .css('position','absolute');
+ return $clone.get(0);
+ }
+ });
+
+ // Observe menu items
+ this.$.find('.add_new_story').click(this.handleNewStoryClick);
+
+ if (this.isSprintBacklog()) {
+ RB.Factory.initialize(RB.Sprint, this.getSprint());
+ this.burndown = RB.Factory.initialize(RB.Burndown, this.$.find('.show_burndown_chart'));
+ this.burndown.setSprintId(this.getSprint().data('this').getID());
+ }
+
+ // Initialize each item in the backlog
+ this.getStories().each(function (index) {
+ // 'this' refers to an element with class="story"
+ RB.Factory.initialize(RB.Story, this);
+ });
+
+ if (this.isSprintBacklog()) {
+ this.refresh();
+ }
+ },
+
+ dragChanged: function (e, ui) {
+ $(this).parents('.backlog').data('this').refresh();
+ },
+
+ dragComplete: function (e, ui) {
+ var isDropTarget = (ui.sender === null || ui.sender === undefined);
+
+ // jQuery triggers dragComplete of source and target.
+ // Thus we have to check here. Otherwise, the story
+ // would be saved twice.
+ if (isDropTarget) {
+ ui.item.data('this').saveDragResult();
+ }
+ },
+
+ dragStart: function (e, ui) {
+ ui.item.addClass("dragging");
+ },
+
+ dragStop: function (e, ui) {
+ ui.item.removeClass("dragging");
+
+ // FIXME: workaround for IE7
+ if ($.browser.msie && $.browser.version <= 7) {
+ ui.item.css("z-index", 0);
+ }
+ },
+
+ getSprint: function () {
+ return $(this.el).find(".model.sprint").first();
+ },
+
+ getStories: function () {
+ return this.getList().children(".story");
+ },
+
+ getList: function () {
+ return this.$.children(".stories").first();
+ },
+
+ handleNewStoryClick: function (e) {
+ var toggler = $(this).parents('.header').find('.toggler');
+ if (toggler.hasClass('closed')){
+ toggler.click();
+ }
+ e.preventDefault();
+ $(this).parents('.backlog').data('this').newStory();
+ },
+
+ // return true if backlog has an element with class="sprint"
+ isSprintBacklog: function () {
+ return $(this.el).find('.sprint').length === 1;
+ },
+
+ newStory: function () {
+ var story, o;
+
+ story = $('#story_template').children().first().clone();
+ this.getList().prepend(story);
+
+ o = RB.Factory.initialize(RB.Story, story[0]);
+ o.edit();
+
+ story.find('.editor').first().focus();
+ },
+
+ refresh : function () {
+ this.recalcVelocity();
+ this.recalcOddity();
+ },
+
+ recalcVelocity: function () {
+ var total;
+
+ if (!this.isSprintBacklog()) {
+ return true;
+ }
+
+ total = 0;
+ this.getStories().each(function (index) {
+ total += $(this).data('this').getPoints();
+ });
+ this.$.children('.header').children('.velocity').text(total);
+ },
+
+ recalcOddity : function () {
+ this.$.find('.story:even').removeClass('odd').addClass('even');
+ this.$.find('.story:odd').removeClass('even').addClass('odd');
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlogs.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlogs.js
new file mode 100644
index 0000000000..6ba2ec9d87
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/backlogs.js
@@ -0,0 +1,54 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+//= require backlogs/jquery.flot/jquery.flot
+//= require backlogs/jquery.flot/excanvas
+//= require backlogs/jquery.jeditable.mini
+//= require backlogs/jquery.cookie
+//= require backlogs/livepipe-ui/livepipe
+//= require backlogs/livepipe-ui/window
+//= require backlogs/common
+//= require backlogs/master_backlog
+//= require backlogs/backlog
+//= require backlogs/burndown
+//= require backlogs/model
+//= require backlogs/editable_inplace
+//= require backlogs/sprint
+//= require backlogs/work_package
+//= require backlogs/story
+//= require backlogs/task
+//= require backlogs/impediment
+//= require backlogs/taskboard
+//= require backlogs/show_main
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/burndown.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/burndown.js
new file mode 100644
index 0000000000..7a00865a84
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/burndown.js
@@ -0,0 +1,79 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+RB.Burndown = (function ($) {
+ return RB.Object.create({
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+
+ // Observe menu items
+ this.$.click(this.show);
+ },
+
+ setSprintId : function (sprintId) {
+ this.sprintId = sprintId;
+ },
+
+ getSprintId : function (){
+ return this.sprintId;
+ },
+
+ show: function (e) {
+ e.preventDefault();
+
+ if ($("#charts").length === 0) {
+ $('
').appendTo("body");
+ }
+ $('#charts').html("" + RB.i18n.generating_graph + "
");
+ $('#charts').load(RB.urlFor('show_burndown_chart', { sprint_id: $(this).data('this').sprintId,
+ project_id: RB.constants.project_id}));
+ $('#charts').dialog({
+ dialogClass: "rb_dialog",
+ height: 530,
+ width: 710,
+ position: 'center',
+ modal: true,
+ title: RB.i18n.burndown_graph,
+ resizable: false
+ });
+ $('.ui-icon-closethick').prop('title', 'close');
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/common.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/common.js
new file mode 100644
index 0000000000..5e48c5ba34
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/common.js
@@ -0,0 +1,166 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+if (window.RB === null || window.RB === undefined) {
+ window.RB = (function ($) {
+ var object, Factory, Dialog, UserPreferences,
+ ajax;
+
+ object = {
+ // Douglas Crockford's technique for object extension
+ // http://javascript.crockford.com/prototypal.html
+ create: function () {
+ var obj, i, methods, methodName;
+
+ function F() {
+ }
+
+ F.prototype = arguments[0];
+ obj = new F();
+
+ // Add all the other arguments as mixins that
+ // 'write over' any existing methods
+ for (i = 1; i < arguments.length; i += 1) {
+ methods = arguments[i];
+ if (typeof methods === 'object') {
+ for (methodName in methods) {
+ if (methods.hasOwnProperty(methodName)) {
+ obj[methodName] = methods[methodName];
+ }
+ }
+ }
+ }
+ return obj;
+ }
+ };
+
+
+ // Object factory for chiliproject_backlogs
+ Factory = object.create({
+
+ initialize: function (objType, el) {
+ var obj;
+
+ obj = object.create(objType);
+ obj.initialize(el);
+ return obj;
+ }
+
+ });
+
+ // Utilities
+ Dialog = object.create({
+ msg: function (msg) {
+ var dialog, baseClasses;
+
+ baseClasses = 'ui-button ui-widget ui-state-default ui-corner-all';
+
+ if ($('#msgBox').size() === 0) {
+ dialog = $('
').appendTo('body');
+ }
+ else {
+ dialog = $('#msgBox');
+ }
+
+ dialog.html(msg);
+ dialog.dialog({
+ title: 'Backlogs Plugin',
+ buttons: [
+ {
+ text: 'OK',
+ class: 'button -highlight',
+ click: function () {
+ $(this).dialog("close");
+ }
+ }],
+ modal: true
+ });
+ $('.button').removeClass(baseClasses);
+ $('.ui-icon-closethick').prop('title', 'close');
+ }
+ });
+
+ ajax = (function () {
+ var ajaxQueue, ajaxOngoing,
+ processAjaxQueue;
+
+ ajaxQueue = [];
+ ajaxOngoing = false;
+
+ processAjaxQueue = function () {
+ var options = ajaxQueue.shift();
+
+ if (options !== null && options !== undefined) {
+ ajaxOngoing = true;
+ $.ajax(options);
+ }
+ };
+
+ // Process outstanding entries in the ajax queue whenever a ajax request
+ // finishes.
+ $(document).ajaxComplete(function (event, xhr, settings) {
+ ajaxOngoing = false;
+ processAjaxQueue();
+ });
+
+ return function (options) {
+ ajaxQueue.push(options);
+ if (!ajaxOngoing) {
+ processAjaxQueue();
+ }
+ };
+ }());
+
+ // Abstract the user preference from the rest of the RB objects
+ // so that we can change the underlying implementation as needed
+ UserPreferences = object.create({
+ get: function (key) {
+ return $.cookie(key);
+ },
+
+ set: function (key, value) {
+ $.cookie(key, value, { expires: 365 * 10 });
+ }
+ });
+
+ return {
+ Object : object,
+ Factory : Factory,
+ Dialog : Dialog,
+ UserPreferences : UserPreferences,
+ ajax : ajax
+ };
+ }(jQuery));
+}
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/editable_inplace.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/editable_inplace.js
new file mode 100644
index 0000000000..837a21be14
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/editable_inplace.js
@@ -0,0 +1,72 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+RB.EditableInplace = (function ($) {
+ return RB.Object.create(RB.Model, {
+
+ displayEditor: function (editor) {
+ this.$.addClass("editing");
+ editor.find(".editor").bind('keyup', this.handleKeyup);
+ },
+
+ getEditor: function () {
+ // Create the model editor container if it does not yet exist
+ var editor = this.$.children(".editors").first().html('');
+
+ if (editor.length === 0) {
+ editor = $("
").appendTo(this.$);
+ }
+ return editor;
+ },
+
+ handleKeyup: function (e) {
+ var j, that;
+
+ j = $(this).parents('.model').first();
+ that = j.data('this');
+
+ switch (e.which) {
+ case 13: // Enter
+ that.saveEdits();
+ break;
+ case 27: // ESC
+ that.cancelEdit();
+ break;
+ default:
+ return true;
+ }
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/impediment.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/impediment.js
new file mode 100644
index 0000000000..63c963dcf9
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/impediment.js
@@ -0,0 +1,86 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/**************************************
+ IMPEDIMENT
+***************************************/
+
+RB.Impediment = (function ($) {
+ return RB.Object.create(RB.Task, {
+
+ initialize: function (el) {
+ var j; // This ensures that we use a local 'j' variable, not a global one.
+
+ this.$ = j = $(el);
+ this.el = el;
+
+ j.addClass("impediment"); // If node is based on #task_template, it doesn't have the impediment class yet
+
+ // Associate this object with the element for later retrieval
+ j.data('this', this);
+
+ j.find(".editable").live('mouseup', this.handleClick);
+ },
+
+ // Override saveDirectives of RB.Task
+ saveDirectives: function () {
+ var j, prev, statusID, data, url;
+
+ j = this.$;
+ prev = this.$.prev();
+ statusID = j.parent('td').first().attr('id').split("_")[1];
+
+ data = j.find('.editor').serialize() +
+ "&is_impediment=true" +
+ "&fixed_version_id=" + RB.constants.sprint_id +
+ "&status_id=" + statusID +
+ "&prev=" + (prev.length === 1 ? prev.data('this').getID() : '') +
+ (this.isNew() ? "" : "&id=" + j.children('.id').text());
+
+ if (this.isNew()) {
+ url = RB.urlFor('create_impediment', {sprint_id: RB.constants.sprint_id});
+ }
+ else {
+ url = RB.urlFor('update_impediment', {id: this.getID(), sprint_id: RB.constants.sprint_id});
+ data += "&_method=put";
+ }
+
+ return {
+ url: url,
+ data: data
+ };
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/master_backlog.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/master_backlog.js
new file mode 100644
index 0000000000..5bd9334b6f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/master_backlog.js
@@ -0,0 +1,58 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+// Initialize the backlogs after DOM is loaded
+jQuery(function ($) {
+
+ // Initialize each backlog
+ $('.backlog').each(function (index) {
+ // 'this' refers to an element with class="backlog"
+ RB.Factory.initialize(RB.Backlog, this);
+ });
+
+ // Workaround for IE7
+ if ($.browser.msie && $.browser.version <= 7) {
+ var z = 50;
+ $('.backlog, .header').each(function () {
+ $(this).css('z-index', z);
+ z -= 1;
+ });
+ }
+
+ $('.backlog .toggler').on('click',function(){
+ $(this).toggleClass('closed');
+ $(this).parents('.backlog').find('ul.stories').toggleClass('closed');
+ });
+});
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/model.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/model.js
new file mode 100644
index 0000000000..cff6162e82
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/model.js
@@ -0,0 +1,497 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/***************************************
+ MODEL
+ Common methods for sprint, work_package,
+ story, task, and impediment
+***************************************/
+
+RB.Model = (function ($) {
+ return RB.Object.create({
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+ },
+
+ afterCreate: function (data, textStatus, xhr) {
+ // Do nothing. Child objects may optionally override this
+ },
+
+ afterSave: function (data, textStatus, xhr) {
+ var isNew, result;
+
+ isNew = this.isNew();
+ result = RB.Factory.initialize(RB.Model, data);
+
+ this.unmarkSaving();
+ this.refresh(result);
+
+ if (isNew) {
+ this.$.attr('id', result.$.attr('id'));
+ this.afterCreate(data, textStatus, xhr);
+ }
+ else {
+ this.afterUpdate(data, textStatus, xhr);
+ }
+ },
+
+ afterUpdate: function (data, textStatus, xhr) {
+ // Do nothing. Child objects may optionally override this
+ },
+
+ beforeSave: function () {
+ // Do nothing. Child objects may or may not override this method
+ },
+
+ cancelEdit: function () {
+ this.endEdit();
+ if (this.isNew()) {
+ this.$.hide('blind');
+ }
+ },
+
+ close: function () {
+ this.$.addClass('closed');
+ },
+
+ copyFromDialog: function () {
+ var editors;
+
+ if (this.$.find(".editors").length === 0) {
+ editors = $("
").appendTo(this.$);
+ }
+ else {
+ editors = this.$.find(".editors").first();
+ }
+ editors.html("");
+ editors.append($("#" + this.getType().toLowerCase() + "_editor").children(".editor"));
+ this.saveEdits();
+ },
+
+ displayEditor: function (editor) {
+ var pos = this.$.offset(),
+ self = this,
+ baseClasses;
+
+ baseClasses = 'ui-button ui-widget ui-state-default ui-corner-all';
+
+ editor.dialog({
+ buttons: [
+ {
+ text: 'OK',
+ class: 'button -highlight',
+ click: function () {
+ self.copyFromDialog();
+ $(this).dialog("close");
+ }
+ },
+ {
+ text: 'Cancel',
+ class: 'button',
+ click: function () {
+ self.cancelEdit();
+ $(this).dialog("close");
+ }
+ },
+ ],
+ close: function (e, ui) {
+ if (e.which === 27) {
+ self.cancelEdit();
+ }
+ },
+ dialogClass: this.getType().toLowerCase() + '_editor_dialog',
+ modal: true,
+ position: [pos.left - $(document).scrollLeft(), pos.top - $(document).scrollTop()],
+ resizable: false,
+ title: (this.isNew() ? this.newDialogTitle() : this.editDialogTitle())
+ });
+ editor.find(".editor").first().focus();
+ $('.button').removeClass(baseClasses);
+ $('.ui-icon-closethick').prop('title', 'close');
+ },
+
+ edit: function () {
+ var editor = this.getEditor(),
+ self = this,
+ maxTabIndex = 0;
+
+ $('.stories .editors .editor').each(function (index) {
+ var value;
+
+ value = parseInt($(this).attr('tabindex'), 10);
+
+ if (maxTabIndex < value) {
+ maxTabIndex = value;
+ }
+ });
+
+ this.$.find('.editable').each(function (index) {
+ var field, fieldType, fieldLabel, fieldName, fieldOrder, input, newInput,
+ typeId, statusId ;
+
+ field = $(this);
+ fieldId = field.attr('field_id');
+ fieldName = field.attr('fieldname');
+ fieldLabel = field.attr('fieldlabel');
+ fieldOrder = parseInt(field.attr('fieldorder'), 10);
+ fieldType = field.attr('fieldtype') || 'input';
+
+ if (!fieldLabel) {
+ fieldLabel = fieldName.replace(/_/ig, " ").replace(/ id$/ig, "");
+ }
+
+ if (fieldType === 'select') {
+ // Special handling for status_id => they are dependent of type_id
+ if (fieldName === 'status_id') {
+ typeId = $.trim(self.$.find('.type_id .v').html());
+ // when creating stories we need to query the select directly
+ if (typeId == '') {
+ typeId = $('#type_id_options').val();
+ }
+ statusId = $.trim(self.$.find('.status_id .v').html());
+ input = self.findFactory(typeId, statusId, fieldName);
+ }
+ else if (fieldName === 'type_id'){
+ input = $('#' + fieldName + '_options').clone(true);
+ // if the type changes the status dropdown has to be modified
+ input.change(function(){
+ typeId = $(this).val();
+ statusId = $.trim(self.$.find('.status_id .v').html());
+ newInput = self.findFactory(typeId, statusId, 'status_id');
+ newInput = self.prepareInputFromFactory(newInput,fieldId,'status_id',fieldOrder,maxTabIndex);
+ newInput = self.replaceStatusForNewType(input, newInput, $(this).parent().find('.status_id').val(), editor);
+ });
+ }
+ else {
+ input = $('#' + fieldName + '_options').clone(true);
+ }
+ }
+ else {
+ input = $(document.createElement(fieldType));
+ }
+
+ input = self.prepareInputFromFactory(input, fieldId, fieldName, fieldOrder, maxTabIndex);
+
+ // Copy the value in the field to the input element
+ input.val(fieldType === 'select' ? field.children('.v').first().text() : field.text());
+
+
+ // Add a date picker if field is a date field
+ if (field.hasClass("date")) {
+ input.datepicker({
+ changeMonth: true,
+ changeYear: true,
+ closeText: 'Close',
+ dateFormat: 'yy-mm-dd',
+ firstDay: 1,
+ showOn: 'button',
+ onClose: function () {
+ $(this).focus();
+ },
+ selectOtherMonths: true,
+ showAnim: '',
+ showButtonPanel: true,
+ showOtherMonths: true
+ });
+
+ // Remove click-bindings from div - since leaving the edit modus removes the input
+ // and creates a new one
+ // Open the datepicker when you click on the div (before in edit-mode)
+ field.unbind("click");
+ field.click(function(){input.datepicker("show");});
+
+ // So that we won't need a datepicker button to re-show it
+ input.mouseup(function () {
+ $(this).datepicker("show");
+ });
+ }
+
+ // Record in the model's root element which input field had the last focus. We will
+ // use this information inside RB.Model.refresh() to determine where to return the
+ // focus after the element has been refreshed with info from the server.
+ input.focus(function () {
+ self.$.data('focus', $(this).attr('name'));
+ });
+
+ input.blur(function () {
+ self.$.data('focus', '');
+ });
+
+ $(" ").attr({
+ for: input.attr('id'),
+ }).text(fieldLabel).appendTo(editor);
+ input.appendTo(editor);
+ });
+
+ this.displayEditor(editor);
+ this.editorDisplayed(editor);
+ return editor;
+ },
+
+ findFactory: function (typeId, statusId, fieldName){
+ // Find a factory
+ newInput = $('#' + fieldName + '_options_' + typeId + '_' + statusId);
+ if (newInput.length === 0) {
+ // when no list found, only offer the default status
+ // no list = combination is not valid / user has no rights -> workflow
+ newInput = $('#status_id_options_default_' + statusId);
+ }
+ newInput = newInput.clone(true);
+ return newInput;
+ },
+
+ prepareInputFromFactory: function (input,fieldId,fieldName,fieldOrder, maxTabIndex) {
+ input.attr('id', fieldName + '_' + fieldId);
+ input.attr('name', fieldName);
+ input.attr('tabindex', fieldOrder + maxTabIndex);
+ input.addClass(fieldName);
+ input.addClass('editor');
+ input.removeClass('template');
+ input.removeClass('helper');
+ return input;
+ },
+
+ replaceStatusForNewType: function (input,newInput, statusId, editor) {
+ // Append an empty field and select it in case the old status is not available
+ newInput.val(statusId); // try to set the status
+ if (newInput.val() !== statusId){
+ newInput.append(new Option('',''));
+ newInput.val('');
+ }
+ newInput.focus(function () {
+ self.$.data('focus', $(this).attr('name'));
+ });
+
+ newInput.blur(function () {
+ self.$.data('focus', '');
+ });
+ // Find the old status dropdown and replace it with the new one
+ input.parent().find('.status_id').replaceWith(newInput);
+ },
+
+ // Override this method to change the dialog title
+ editDialogTitle: function () {
+ return "Edit " + this.getType();
+ },
+
+ editorDisplayed: function (editor) {
+ // Do nothing. Child objects may override this.
+ },
+
+ endEdit: function () {
+ this.$.removeClass('editing');
+ },
+
+ error: function (xhr, textStatus, error) {
+ this.markError();
+ RB.Dialog.msg($(xhr.responseText).find('.errors').html());
+ this.processError(xhr, textStatus, error);
+ },
+
+ getEditor: function () {
+ var editorId, editor;
+ // Create the model editor if it does not yet exist
+ editorId = this.getType().toLowerCase() + "_editor";
+
+ editor = $("#" + editorId).html("");
+
+ if (editor.length === 0) {
+ editor = $("
").appendTo("body");
+ }
+ return editor;
+ },
+
+ getID: function () {
+ return this.$.children('.id').children('.v').text();
+ },
+
+ getType: function () {
+ throw "Child objects must override getType()";
+ },
+
+ handleClick: function (e) {
+ var field, model, j, editor;
+
+ field = $(this);
+ model = field.parents('.model').first().data('this');
+ j = model.$;
+
+ if (!j.hasClass('editing') && !j.hasClass('dragging') && !j.hasClass('prevent_edit') && !$(e.target).hasClass('prevent_edit')) {
+ editor = model.edit();
+ editor.find('.' + $(e.currentTarget).attr('fieldname') + '.editor').focus();
+ }
+ },
+
+ handleSelect: function (e) {
+ var j = $(this),
+ self = j.data('this');
+
+ if (!$(e.target).hasClass('editable') &&
+ !$(e.target).hasClass('checkbox') &&
+ !j.hasClass('editing') &&
+ e.target.tagName !== 'A' &&
+ !j.hasClass('dragging')) {
+
+ self.setSelection(!self.isSelected());
+ }
+ },
+
+ isClosed: function () {
+ return this.$.hasClass('closed');
+ },
+
+ isNew: function () {
+ return this.getID() === "";
+ },
+
+ markError: function () {
+ this.$.addClass('error icon icon-bug');
+ },
+
+ markIfClosed: function () {
+ throw "Child objects must override markIfClosed()";
+ },
+
+ markSaving: function () {
+ this.$.addClass('saving');
+ },
+
+ // Override this method to change the dialog title
+ newDialogTitle: function () {
+ return "New " + this.getType();
+ },
+
+ open: function () {
+ this.$.removeClass('closed');
+ },
+
+ processError: function (x, t, e) {
+ // Override as needed
+ },
+
+ refresh: function (obj) {
+ this.$.html(obj.$.html());
+
+ if (obj.$.length > 1) {
+ // execute script tags, that were attached to the sources
+ obj.$.filter('script').each(function () {
+ try {
+ $.globalEval($(this).html());
+ }
+ catch (e) {
+ }
+ });
+ }
+
+ if (obj.isClosed()) {
+ this.close();
+ } else {
+ this.open();
+ }
+ this.refreshed();
+ },
+
+ refreshed: function () {
+ // Override as needed
+ },
+
+ saveDirectives: function () {
+ throw "Child object must implement saveDirectives()";
+ },
+
+ saveEdits: function () {
+ var j = this.$,
+ self = this,
+ editors = j.find('.editor'),
+ saveDir;
+
+ // Copy the values from the fields to the proper html elements
+ editors.each(function (index) {
+ var editor, fieldName;
+
+ editor = $(this);
+ fieldName = editor.attr('name');
+ if (this.type.match(/select/)) {
+ // if the user changes the type and that type does not offer the status
+ // of the current story, the status field is set to blank
+ // if the user saves this edit we will receive a validation error
+ // the following 3 lines will prevent the override of the status id
+ // otherwise we would loose the status id of the current ticket
+ if (!(editor.val() === '' && fieldName === 'status_id')){
+ j.children('div.' + fieldName).children('.v').text(editor.val());
+ }
+
+ j.children('div.' + fieldName).children('.t').text(editor.children(':selected').text());
+
+ } else {
+ j.children('div.' + fieldName).text(editor.val());
+ }
+ });
+
+ // Mark the work_package as closed if so
+ self.markIfClosed();
+
+ // Get the save directives.
+ saveDir = self.saveDirectives();
+
+ self.beforeSave();
+
+ self.unmarkError();
+ self.markSaving();
+ RB.ajax({
+ type: "POST",
+ url: saveDir.url,
+ data: saveDir.data,
+ success : function (d, t, x) {
+ self.afterSave(d, t, x);
+ },
+ error : function (x, t, e) {
+ self.error(x, t, e);
+ }
+ });
+ self.endEdit();
+ },
+
+ unmarkError: function () {
+ this.$.removeClass('error icon icon-bug');
+ },
+
+ unmarkSaving: function () {
+ this.$.removeClass('saving');
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/show_main.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/show_main.js
new file mode 100644
index 0000000000..67e732cdd6
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/show_main.js
@@ -0,0 +1,53 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+// Initialize everything after DOM is loaded
+jQuery(function ($) {
+ var defaultDialogColor; // this var is used as cache for some computation in
+ // the inner function. -> Do not move to where it
+ // actually belongs!
+
+ RB.Factory.initialize(RB.Taskboard, $('#taskboard'));
+
+ $('#assigned_to_id_options').change(function () {
+ var selected = $(this).children(':selected');
+ if (!defaultDialogColor) {
+ // fetch the color from the task rendered as a prototype/template for new tasks
+ defaultDialogColor = $('#work_package_').css('background-color');
+ }
+ $(this).parents('.ui-dialog').css('background-color', selected.attr('color') || defaultDialogColor);
+ $(this).parents('.ui-dialog').colorcontrast();
+ });
+});
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/sprint.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/sprint.js
new file mode 100644
index 0000000000..0b52ef400d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/sprint.js
@@ -0,0 +1,84 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/***************************************
+ SPRINT
+***************************************/
+
+RB.Sprint = (function ($) {
+ return RB.Object.create(RB.Model, RB.EditableInplace, {
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+ this.$.find(".editable").mouseup(this.handleClick);
+ },
+
+ beforeSave: function () {
+ // Do nothing
+ },
+
+ getType: function () {
+ return "Sprint";
+ },
+
+ markIfClosed: function () {
+ // Do nothing
+ },
+
+ refreshed: function () {
+ // We have to do this since .live() does not work for some reason
+ this.$.find(".editable").mouseup(this.handleClick);
+ },
+
+ saveDirectives: function () {
+ var j = this.$,
+ data = j.find('.editor').serialize() + "&_method=put",
+ url = RB.urlFor('update_sprint', { id: this.getID() });
+
+ return {
+ url : url,
+ data: data
+ };
+ },
+
+ beforeSaveDragResult: function () {
+ // Do nothing
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/story.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/story.js
new file mode 100644
index 0000000000..d7ef302b56
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/story.js
@@ -0,0 +1,138 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/**************************************
+ STORY
+***************************************/
+RB.Story = (function ($) {
+ return RB.Object.create(RB.WorkPackage, RB.EditableInplace, {
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+ this.$.find(".editable").live('mouseup', this.handleClick);
+ },
+
+ /**
+ * Callbacks from model.js
+ **/
+ beforeSave: function () {
+ this.refreshStory();
+ },
+
+ afterCreate: function (data, textStatus, xhr) {
+ this.refreshStory();
+ },
+
+ afterUpdate : function (data, textStatus, xhr) {
+ this.refreshStory();
+ },
+
+ refreshed: function () {
+ this.refreshStory();
+ },
+ /**/
+
+ editDialogTitle: function () {
+ return "Story #" + this.getID();
+ },
+
+ editorDisplayed: function (editor) { },
+
+ getPoints: function () {
+ var points = parseInt(this.$.find('.story_points').first().text(), 10);
+ return isNaN(points) ? 0 : points;
+ },
+
+ getType: function () {
+ return "Story";
+ },
+
+ markIfClosed: function () {
+ // Do nothing
+ },
+
+ newDialogTitle: function () {
+ return "New Story";
+ },
+
+ refreshStory : function () {
+ this.recalcVelocity();
+ },
+
+ recalcVelocity: function () {
+ this.$.parents(".backlog").first().data('this').refresh();
+ },
+
+ saveDirectives: function () {
+ var url, prev, sprintId, data;
+
+ prev = this.$.prev();
+ sprintId = this.$.parents('.backlog').data('this').isSprintBacklog() ?
+ this.$.parents('.backlog').data('this').getSprint().data('this').getID() :
+ '';
+
+ data = "prev=" +
+ (prev.length === 1 ? prev.data('this').getID() : '') +
+ "&fixed_version_id=" + sprintId;
+
+ if (this.$.find('.editor').length > 0) {
+ data += "&" + this.$.find('.editor').serialize();
+ }
+
+ //TODO: this might be unsave in case the parent of this story is not the
+ // sprint backlog, then we dont have a sprintId an cannot generate a
+ // valid url - one option might be to take RB.constants.sprint_id
+ // hoping it exists
+ if (this.isNew()) {
+ url = RB.urlFor('create_story', {sprint_id: sprintId});
+ } else {
+ url = RB.urlFor('update_story', {id: this.getID(), sprint_id: sprintId});
+ data += "&_method=put";
+ }
+
+ return {
+ url: url,
+ data: data
+ };
+ },
+
+ beforeSaveDragResult: function () {
+ // Do nothing
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/task.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/task.js
new file mode 100644
index 0000000000..18690b3d93
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/task.js
@@ -0,0 +1,124 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/**************************************
+ TASK
+***************************************/
+
+RB.Task = (function ($) {
+ return RB.Object.create(RB.WorkPackage, {
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+
+ // If node is based on #task_template, it doesn't have the story class yet
+ this.$.addClass("task");
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+ this.$.find(".editable").live('mouseup', this.handleClick);
+ this.defaultColor = $('#rb .task').css('background-color');
+ },
+
+ beforeSave: function name() {
+ if (this.el && $(this.el).hasClass('dragging')){
+ return;
+ }
+ var c = this.$.find('select.assigned_to_id').children(':selected').attr('color') || this.defaultColor;
+ this.$.css('background-color', c);
+ this.$.colorcontrast();
+ },
+
+ editorDisplayed: function (dialog) {
+ dialog.parents('.ui-dialog').css('background-color', this.$.css('background-color'));
+ dialog.parents('.ui-dialog').colorcontrast();
+ },
+
+ getType: function () {
+ return "Task";
+ },
+
+ markIfClosed: function () {
+ if (this.$.parent('td').first().hasClass('closed')) {
+ this.$.addClass('closed');
+ } else {
+ this.$.removeClass('closed');
+ }
+ },
+
+ saveDirectives: function () {
+ var prev, cellId, data, url;
+
+ prev = this.$.prev();
+ cellId = this.$.parent('td').first().attr('id').split("_");
+
+ data = this.$.find('.editor').serialize() +
+ "&parent_id=" + cellId[0] +
+ "&status_id=" + cellId[1] +
+ "&prev=" + (prev.length === 1 ? prev.data('this').getID() : '') +
+ (this.isNew() ? "" : "&id=" + this.$.children('.id').text());
+
+ if (this.isNew()) {
+ url = RB.urlFor('create_task', {sprint_id: RB.constants.sprint_id});
+ }
+ else {
+ url = RB.urlFor('update_task', {id: this.getID(), sprint_id: RB.constants.sprint_id});
+ data += "&_method=put";
+ }
+
+ return {
+ url: url,
+ data: data
+ };
+ },
+
+ beforeSaveDragResult: function () {
+ if (this.$.parent('td').first().hasClass('closed')) {
+ // This is only for the purpose of making the Remaining Hours reset
+ // instantaneously after dragging to a closed status. The server should
+ // still make sure to reset the value.
+ this.$.children('.remaining_hours.editor').val('');
+ this.$.children('.remaining_hours.editable').text('');
+ }
+ },
+
+ refreshed : function () {
+ var remainingHours = this.$.children('.remaining_hours.editable');
+
+ remainingHours.toggleClass('empty', remainingHours.is(':empty'));
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/taskboard.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/taskboard.js
new file mode 100644
index 0000000000..1fc502b180
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/taskboard.js
@@ -0,0 +1,192 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/***************************************
+ TASKBOARD
+***************************************/
+
+RB.Taskboard = (function ($) {
+ return RB.Object.create(RB.Model, {
+
+ initialize: function (el) {
+ var self = this; // So we can bind the event handlers to this object
+
+ this.$ = $(el);
+ this.el = el;
+
+ // Associate this object with the element for later retrieval
+ this.$.data('this', this);
+
+ // Initialize column widths
+ this.colWidthUnit = $(".swimlane").width();
+ this.defaultColWidth = 1;
+ this.loadColWidthPreference();
+ this.updateColWidths();
+
+ $("#col_width input").keyup(function (e) {
+ if (e.which === 13) {
+ self.updateColWidths();
+ }
+ });
+
+ this.initializeTasks();
+ this.initializeImpediments();
+
+ this.initializeNewButtons();
+ this.initializeSortables();
+ },
+
+ initializeNewButtons : function () {
+ this.$.find('#tasks .add_new.clickable').click(this.handleAddNewTaskClick);
+ this.$.find('#impediments .add_new.clickable').click(this.handleAddNewImpedimentClick);
+ },
+
+ initializeSortables : function () {
+ this.$.find('#impediments .list').sortable({
+ placeholder: 'placeholder',
+ start: this.dragStart,
+ stop: this.dragStop,
+ update: this.dragComplete,
+ cancel: '.prevent_edit'
+ }).sortable('option', 'connectWith', '#impediments .list');
+ $('#impediments .list').disableSelection();
+
+ var list, augmentList, self = this;
+
+ list = this.$.find('#tasks .list');
+
+ augmentList = function () {
+ $(list.splice(0, 50)).sortable({
+ placeholder: 'placeholder',
+ start: self.dragStart,
+ stop: self.dragStop,
+ update: self.dragComplete,
+ cancel: '.prevent_edit'
+ }).sortable('option', 'connectWith', '#tasks .list');
+ $('#tasks .list').disableSelection();
+
+ if (list.length > 0) {
+ /*globals setTimeout*/
+ setTimeout(augmentList, 10);
+ }
+ };
+ augmentList();
+ },
+
+ initializeTasks : function () {
+ this.$.find('.task').each(function (index) {
+ RB.Factory.initialize(RB.Task, this);
+ });
+ },
+
+ initializeImpediments : function () {
+ this.$.find('.impediment').each(function (index) {
+ RB.Factory.initialize(RB.Impediment, this);
+ });
+ },
+
+ dragComplete: function (e, ui) {
+ // Handler is triggered for source and target. Thus the need to check.
+ var isDropTarget = (ui.sender === null);
+
+ if (isDropTarget) {
+ ui.item.data('this').saveDragResult();
+ }
+ },
+
+ dragStart: function (e, ui) {
+ ui.item.addClass("dragging");
+ },
+
+ dragStop: function (e, ui) {
+ ui.item.removeClass("dragging");
+
+ // FIXME: workaround for IE7
+ if ($.browser.msie && $.browser.version <= 7) {
+ ui.item.css("z-index", 0);
+ }
+ },
+
+ handleAddNewImpedimentClick: function (e) {
+ var row = $(this).parents("tr").first();
+ $('#taskboard').data('this').newImpediment(row);
+ },
+
+ handleAddNewTaskClick: function (e) {
+ var row = $(this).parents("tr").first();
+ $('#taskboard').data('this').newTask(row);
+ },
+
+ loadColWidthPreference: function () {
+ var w = RB.UserPreferences.get('taskboardColWidth');
+ if (w === null || w === undefined) {
+ w = this.defaultColWidth;
+ RB.UserPreferences.set('taskboardColWidth', w);
+ }
+ $("#col_width input").val(w);
+ },
+
+ newImpediment: function (row) {
+ var impediment, o;
+
+ impediment = $('#impediment_template').children().first().clone();
+ row.find(".list").first().prepend(impediment);
+
+ o = RB.Factory.initialize(RB.Impediment, impediment);
+ o.edit();
+ },
+
+ newTask: function (row) {
+ var task, o;
+
+ task = $('#task_template').children().first().clone();
+ row.find(".list").first().prepend(task);
+
+ o = RB.Factory.initialize(RB.Task, task);
+ o.edit();
+ },
+
+ updateColWidths: function () {
+ var w = parseInt($("#col_width input").val(), 10);
+
+ if (isNaN(w) || w <= 0) {
+ w = this.defaultColWidth;
+ }
+ $("#col_width input").val(w);
+ RB.UserPreferences.set('taskboardColWidth', w);
+ $(".swimlane").width(this.colWidthUnit * w).css('min-width', this.colWidthUnit * w);
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/work_package.js b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/work_package.js
new file mode 100644
index 0000000000..e5dce72176
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/javascripts/backlogs/work_package.js
@@ -0,0 +1,62 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+/**************************************
+ WORK PACKAGE
+***************************************/
+RB.WorkPackage = (function ($) {
+ return RB.Object.create(RB.Model, {
+
+ initialize: function (el) {
+ this.$ = $(el);
+ this.el = el;
+ },
+
+ beforeSaveDragResult: function () {
+ // Do nothing
+ },
+
+ getType: function () {
+ return "WorkPackage";
+ },
+
+ saveDragResult: function () {
+ this.beforeSaveDragResult();
+ if (!this.$.hasClass('editing')) {
+ this.saveEdits();
+ }
+ }
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/backlogs.css.sass b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/backlogs.css.sass
new file mode 100644
index 0000000000..7ec2fe30a9
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/backlogs.css.sass
@@ -0,0 +1,40 @@
+/*-- copyright
+ * OpenProject Backlogs Plugin
+ *
+ * Copyright (C)2013 the OpenProject Foundation (OPF)
+ * Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+ * Copyright (C)2010-2011 friflaj
+ * Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+ * Copyright (C)2009-2010 Mark Maglana
+ * Copyright (C)2009 Joe Heck, Nate Lowrie
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License version 3.
+ *
+ * OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+ * The copyright follows:
+ * Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+ * Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See doc/COPYRIGHT.rdoc for more details.
+ */
+
+@import backlogs/global
+@import backlogs/global_print
+@import backlogs/livepipe-ui/base
+@import backlogs/jqplot
+@import backlogs/statistics
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global.css b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global.css
new file mode 100644
index 0000000000..f8dc2c581b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global.css
@@ -0,0 +1,66 @@
+/*-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+#rb .meta {
+ display:none;
+}
+#rb #helpers {
+ display:none;
+}
+/*
+ .editor is the classname for field editors of sprint,
+ story, task, impediment. These field editors get created
+ at runtime whenever any of the above models are edited.
+*/
+#rb .editors {
+ display:none;
+}
+#rb .ui-dialog .editor {
+ display:block;
+}
+
+/* dialog */
+.ui-dialog .ui-dialog-title { float:left; margin-right:0; }
+.ui-dialog.ui-widget-content { border:none; }
+.ui-dialog .ui-dialog-buttonpane.ui-widget-content { border:none; }
+
+.subject-input {
+ width: 99%;
+}
+
+th {
+ font-weight: bold;
+}
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global_print.css b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global_print.css
new file mode 100644
index 0000000000..e461b60e25
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/global_print.css
@@ -0,0 +1,39 @@
+/*-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+#toolbar .links{
+ display:none !important;
+}
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/jqplot.css b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/jqplot.css
new file mode 100644
index 0000000000..7878583104
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/jqplot.css
@@ -0,0 +1,37 @@
+/*-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+.jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em;}.jqplot-axis{font-size:.75em;}.jqplot-xaxis{margin-top:10px;}.jqplot-x2axis{margin-bottom:10px;}.jqplot-yaxis{margin-right:10px;}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis{margin-left:10px;margin-right:10px;}.jqplot-axis-tick,.jqplot-xaxis-tick,.jqplot-yaxis-tick,.jqplot-x2axis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{position:absolute;}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top;}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom;}.jqplot-yaxis-tick{right:0;top:15px;text-align:right;}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left;}.jqplot-xaxis-label{margin-top:10px;font-size:11pt;position:absolute;}.jqplot-x2axis-label{margin-bottom:10px;font-size:11pt;position:absolute;}.jqplot-yaxis-label{margin-right:10px;font-size:11pt;position:absolute;}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;position:absolute;}table.jqplot-table-legend,table.jqplot-cursor-legend{background-color:rgba(255,255,255,0.6);border:1px solid #ccc;position:absolute;font-size:.75em;}td.jqplot-table-legend{vertical-align:middle;}td.jqplot-table-legend>div{border:1px solid #ccc;padding:.2em;}div.jqplot-table-legend-swatch{width:0;height:0;border-top-width:.35em;border-bottom-width:.35em;border-left-width:.6em;border-right-width:.6em;border-top-style:solid;border-bottom-style:solid;border-left-style:solid;border-right-style:solid;}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em;}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em;}.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,0.5);padding:1px;}.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,0.5);padding:1px;}.jqplot-point-label{font-size:.75em;}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center;}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em;}
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/livepipe-ui/base.css.erb b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/livepipe-ui/base.css.erb
new file mode 100644
index 0000000000..7ea5fceb25
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/livepipe-ui/base.css.erb
@@ -0,0 +1,126 @@
+/*-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+#control_overlay {
+ background-color:#000;
+}
+
+.modal {
+ background-color:#fff;
+ padding:10px;
+ border:1px solid #333;
+}
+
+.tooltip {
+ border:1px solid #000;
+ background-color:#fff;
+ height:25px;
+ width:200px;
+ font-family:"Lucida Grande",Verdana;
+ font-size:10px;
+ color:#333;
+}
+
+.simple_window {
+ width:250px;
+ height:50px;
+ border:1px solid #000;
+ background-color:#fff;
+ padding:10px;
+ text-align:left;
+ font-family:"Lucida Grande",Verdana;
+ font-size:12px;
+ color:#333;
+}
+
+.window {
+ background-image:url(<%= asset_path 'backlogs/livepipe-ui/window_background.png' %>);
+ background-position:top left;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+ padding:10px;
+ font-family:"Lucida Grande",Verdana;
+ font-size:13px;
+ font-weight:bold;
+ color:#fff;
+ text-align:center;
+ min-width:150px;
+ min-height:100px;
+}
+
+.window .window_contents {
+ margin-top:10px;
+ width:100%;
+ height:100%;
+}
+
+.window .window_header {
+ text-align:center;
+}
+
+.window .window_title {
+ margin-top:-7px;
+ margin-bottom:7px;
+ font-size:11px;
+ cursor:move;
+}
+
+.window .window_close {
+ display:block;
+ position:absolute;
+ top:4px;
+ left:5px;
+ height:13px;
+ width:13px;
+ background-image: url(<%= asset_path 'backlogs/livepipe-ui/window_close.gif' %>);
+ cursor:pointer;
+ cursor:hand;
+}
+
+
+/**
+ * Custom Additions
+ **/
+
+#livepipe-modal-closer {
+ width: 33px;
+ height: 33px;
+ background-image: url(<%= asset_path 'backlogs/livepipe-ui/modal_close.png' %>);
+ position: absolute;
+ top: -15px;
+ right: -15px;
+ cursor: pointer;
+}
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/master_backlog.css.sass b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/master_backlog.css.sass
new file mode 100644
index 0000000000..9db9ab4f53
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/master_backlog.css.sass
@@ -0,0 +1,338 @@
+/*-- copyright
+ * OpenProject is a project management system.
+ * Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 3.
+ *
+ * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ * Copyright (C) 2006-2013 Jean-Philippe Lang
+ * Copyright (C) 2010-2013 the ChiliProject Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See doc/COPYRIGHT.rdoc for more details. ++
+ */
+
+@import open_project_global/all
+
+
+#rb
+ #backlogs_container
+ width: 100%
+ min-width: 950px
+ #owner_backlogs_container
+ float: right
+ width: 49.5%
+ min-width: 470px
+ #sprint_backlogs_container
+ float: left
+ width: 49.5%
+ min-width: 470px
+ min-height: 230px
+ #owner_backlogs_container .backlog .header > .add_new_story
+ height: 28px
+ line-height: 31px
+ padding: 0
+ position: absolute
+ right: 10px
+ text-align: right
+ top: 1px
+ width: 100px
+ #backlogs_container .backlog
+ border: 1px solid #E4E4E4
+ display: block
+ margin: 0 0 10px 0
+ width: 100%
+
+/*
+ *this adds space at the bottom of the main content div to leave enough space
+ *for the menu without cutting it even if the bottom backlog does not contain
+ *any elements
+ */
+
+.controller-rb_master_backlogs.action-index #content
+ padding-bottom: 180px
+
+#rb
+ #backlogs_container .backlog .header
+ background-color: #eee
+ height: 30px
+ position: relative
+ width: 100%
+ .backlog .header .menu
+ border-right: 1px solid #E4E4E4
+ cursor: pointer
+ height: 30px
+ overflow: visible
+ position: absolute
+ top: 0
+ left: 0
+ width: 29px
+ &.hover, &:hover
+ width: 30px
+ z-index: 1000
+ .ui-icon
+ position: absolute
+ top: 7px
+ left: 7px
+ .items
+ display: none
+ &.hover .items, &:hover .items
+ background-color: #EEEEEE
+ border: 1px solid #E4E4E4
+ display: block
+ position: absolute
+ top: 30px
+ left: 0
+ list-style: none
+ margin: 0
+ padding: 0
+ .item
+ display: block
+ width: 160px
+ height: 2rem
+ font-size: 0.9rem
+ text-align: left
+ text-decoration: none
+ vertical-align: middle
+ overflow: hidden
+ white-space: nowrap
+ &.hover, &:hover
+ background-color: #999
+ a
+ display: block
+ height: 100%
+ padding: 6px
+ width: 100%
+ &.hover a, &:hover a
+ color: #FFFFFF
+ text-decoration: none
+ #backlogs_container
+ .backlog
+ .header
+ .velocity
+ height: 28px
+ line-height: 31px
+ padding: 0 3px 0 9px
+ position: absolute
+ right: 25px
+ text-align: right
+ top: 0px
+ width: 28px
+ .toggler
+ font-family: "openproject-icon-font"
+ height: 30px
+ line-height: 31px
+ padding: 0
+ position: absolute
+ right: 0
+ top: 0
+ width: 23px
+ cursor: pointer
+ &:before
+ content: "\e08a"
+ margin-left: 7px
+ &.closed:before
+ content: "\e089"
+ margin-left: 7px
+ &:hover
+ cursor: pointer
+ background-color: #D8D8D8
+ .sprint
+ background-color: transparent
+ cursor: pointer
+ display: block
+ height: 29px
+ width: auto
+ margin-left: 30px
+ margin-right: 50px
+ &.saving
+ background-image: image-url("loading.gif")
+ background-repeat: no-repeat
+ background-position: center
+ &.error.icon-bug
+ background: none
+ text-align: center
+ &:before
+ position: absolute
+ color: red
+ .id, .status
+ display: none
+
+ .name
+ line-height: 2rem
+ font-weight: bold
+ overflow: hidden
+ white-space: nowrap
+ margin-left: 0.5em
+
+ .start_date, .effective_date
+ float: right
+ height: 28px
+ line-height: 2rem
+ width: 6.5em
+ margin-left: 0.5em
+ .stories
+ list-style: none
+ min-height: 2rem
+ margin: 0
+ padding: 0 0 0px 0
+ z-index: 500
+ overflow-y: auto
+ overflow-x: hidden
+ &.closed
+ display: none
+ .stories .story
+ cursor: move
+ display: block
+ font-size: 0.9rem
+ margin: 0
+ overflow: hidden
+ position: relative
+ width: 100%
+ &.odd
+ background-color: #F6F7F8
+ &.even
+ background-color: #FFF
+ &.saving
+ background-image: image-url("loading.gif")
+ background-repeat: no-repeat
+ background-position: center
+ &.error.icon-bug
+ background: none
+ text-align: center
+ &:before
+ position: absolute
+ color: red
+ &.hover, &:hover
+ background-color: rgb(254, 248, 168)
+ .id
+ float: left
+ margin-left: 1em
+ margin-right: 1em
+ padding: 5px 2px 4px 2px
+ width: 4em
+ text-align: right
+ white-space: nowrap
+ .type_id .t
+ float: left
+ padding: 5px 2px 4px 2px
+ text-align: right
+ white-space: nowrap
+ .subject
+ overflow: hidden
+ margin-left: 4em
+ padding: 5px 2px 4px 2px
+ white-space: nowrap
+ min-height: 1em
+ .status_id
+ float: right
+ padding: 5px 2px 4px 2px
+ margin-left: 1em
+ width: 68px
+ .story_points
+ float: right
+ padding: 5px 3px 4px 2px
+ margin-right: 2.1em
+ width: 28px
+ min-height: 14px
+ text-align: right
+ .type_id .v, .id .v, .status_id .v, .fixed_version_id, .higher_item_id
+ display: none
+
+.rb_dialog
+ .burndown_chart
+ margin-top: 20px
+ margin-bottom: 20px
+ margin-left: 20px
+ #charts
+ h3
+ border: 0px
+ overflow: hidden
+ fieldset.burndown_control
+ padding-left: 10px
+ border: none
+ border-top: 1px solid #BBB
+ .axislabel
+ font-weight: bold
+
+/* In-place Sprint Editor */
+
+#rb #backlogs_container
+ .sprint.editing
+ .editors, > .editor
+ display: block
+ label, > *
+ display: none
+ +
+ .velocity, .add_new_story
+ display: none
+ .backlog .sprint.editing
+ .name.editor
+ line-height: 1.5rem
+ margin: 0
+ min-width: 15em
+ width: 57%
+ .start_date.editor, .effective_date.editor
+ font-size: 0.9rem
+ line-height: 1.5rem
+ margin: 0
+ padding: 0
+ width: 75px
+ .stories .story.editing
+ >
+ *, .editors label
+ display: none
+ .editors
+ display: block
+ select, input
+ display: inline-block
+ float: none
+ margin: 5px 3px 4px 2px
+ font-size: 0.8rem
+ // reset the line-height (foundation sets it to "normal" but that does not work here)
+ line-height: inherit
+ .type_id.editor
+ width: 15%
+ /* sets max-width for IE */
+ max-width: 140px
+ /* for the cool guys */
+ .subject.editor
+ width: 60%
+ .status_id.editor
+ width: 15%
+ float: right
+ .story_points.editor
+ float: right
+ width: 5%
+
+/* In-place Story Editor */
+
+.controller-rb_master_backlogs.action-show
+ #main, #content, #rb #backlogs_container .backlog .stories
+ overflow: visible
+ div.calendar
+ z-index: 10000
+
+.backlog
+ font-size: 0.9rem
+
+/* Hide the button text in the ui-dialog */
+
+.ui-dialog
+ .ui-dialog-titlebar-close
+ .ui-button-text
+ display: none
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/statistics.css b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/statistics.css
new file mode 100644
index 0000000000..876fa2b4c0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/statistics.css
@@ -0,0 +1,48 @@
+/*-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+.score { text-align: center; width: 1.5em; font-size: large; display: inline-block; }
+.score_0 { background-color: #FF0000; }
+.score_1 { background-color: #FF5300; }
+.score_2 { background-color: #FF8100; }
+.score_3 { background-color: #FFA100; }
+.score_4 { background-color: #FFBB00; }
+.score_5 { background-color: #FFD300; }
+.score_6 { background-color: #FFEC00; }
+.score_7 { background-color: #E9FB00; }
+.score_8 { background-color: #B1F100; }
+.score_9 { background-color: #74E600; }
+.score_10 { background-color: #00CC00; }
diff --git a/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/taskboard.css.sass b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/taskboard.css.sass
new file mode 100644
index 0000000000..e6216b7d90
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/assets/stylesheets/backlogs/taskboard.css.sass
@@ -0,0 +1,363 @@
+/*-- copyright
+ * OpenProject is a project management system.
+ * Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 3.
+ *
+ * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ * Copyright (C) 2006-2013 Jean-Philippe Lang
+ * Copyright (C) 2010-2013 the ChiliProject Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See doc/COPYRIGHT.rdoc for more details. ++
+ */
+
+@import open_project_global/all
+@import helpers/functions
+
+@mixin story-header
+ background-color: #FFFFFF
+ font-size: 1rem - rem-calc(5px)
+ opacity: 0.8
+ filter: alpha(opacity = 80)
+ overflow: hidden
+ padding-bottom: 1px
+ padding-right: 3px
+
+@mixin story-footer
+ float: left
+ font-size: 1rem - rem-calc(5px)
+ width: 85%
+ margin-top: 4px
+ padding: 2px
+ padding-top: 0
+
+@mixin ellipsis
+ overflow: hidden
+ white-space: nowrap
+ text-overflow: ellipsis
+
+
+.controller-rb_taskboards.action-show #main
+ overflow: visible
+
+#taskboard
+ overflow-x: auto
+
+#rb .task
+ color: #484848
+ line-height: inherit
+ white-space: inherit
+
+#rb
+ #assigned_to_id_options
+ display: none
+ .swimlane
+ min-width: 105px
+ /* width + (2*margin) + (2*padding) + (2*border) of .work_package */
+ padding: 5px 0 0 5px
+ width: 105px
+ /* Must be the same as min-width */
+ #board_header
+ background-color: #EBEBEB
+ border: 1px solid #CCCCCC
+ margin-bottom: 0
+ margin-right: 10px
+ td
+ background-color: #EBEBEB
+ border-right: 1px dotted #CFCFCF
+ color: black
+ font-weight: bold
+ text-align: center
+ vertical-align: middle
+ padding-top: 0
+ padding-bottom: 0
+ line-height: 30px
+ &:first-child
+ min-width: 241px
+ width: 241px
+
+ .board
+ background-color: #FCFCFC
+ border: 1px solid #CCCCCC
+ border-top: none
+ margin-right: 10px
+ /* IE7 table fix */
+ table-layout: fixed
+ border-collapse: collapse
+ empty-cells: show
+ tr:hover
+ background-color: #ffffff
+ td
+ border-right: 1px dotted #CFCFCF
+ border-bottom: 1px dotted #CFCFCF
+ vertical-align: top
+ &:first-child
+ min-width: 210px
+ padding: 5px
+ width: 210px
+ tr:last-child td
+ border-bottom: none
+ .add_new
+ margin: 0
+ min-width: 30px
+ padding: 0
+ text-align: center
+ vertical-align: middle
+ width: 30px
+ &.clickable:hover
+ cursor: pointer
+ background-color: #ffffcc
+ .story, .label_sprint_impediments
+ background-color: #F8F6A5
+ border: none
+ display: block
+ float: left
+ height: 80px
+ margin: 5px
+ padding: 5px
+ position: relative
+ width: 190px
+ .story
+ .subject
+ height: 42px
+ line-height: 13px
+ margin-top: 0
+ overflow: hidden
+ padding: 2px
+ width: 180px
+ &.closed .subject
+ text-decoration: line-through
+ .work_package, .placeholder
+ background-color: #AFAFAF
+ border: none
+ cursor: move
+ display: block
+ float: left
+ font-size: 10px
+ height: 80px
+ margin: 5px
+ padding: 5px
+ position: relative
+ width: 85px
+ .work_package.prevent_edit
+ cursor: default
+ .placeholder
+ background-color: #FFFF00
+ border: 1px dashed #333300
+ height: 78px
+ width: 83px
+ .work_package
+ &.closed .subject.editable
+ text-decoration: line-through
+ .v
+ display: none
+ .remaining_hours.editable
+ border: 2px solid #FFFFFF
+ background-color: #EE0000
+ bottom: -5px
+ color: #FFFFFF
+ font-size: 9px
+ height: 13px
+ padding-left: 5px
+ padding-right: 5px
+ position: absolute
+ right: -5px
+ .blocks, .remaining_hours.editable.empty
+ display: none
+ .indicator
+ display: none
+
+ &.saving .indicator
+ background-color: #FFFFFF
+ background-position: center
+ background-repeat: no-repeat
+ border: 2px solid #000000
+ height: 16px
+ padding: 2px
+ position: absolute
+ left: 36px
+ top: 38px
+ width: 16px
+ background-image: image-url("loading.gif")
+ display: block
+
+ &.error .indicator
+ background: none
+ border: none
+
+ &.error.icon-bug:before
+ position: absolute
+ top: 30px
+ left: 28px
+ color: red
+
+
+ .editors
+ display: none
+
+/*
+ * swimlane class is used by:
+ * - #board_header
+ * - .board
+ *
+ * Also use by the Column Width preference to determine the unit width of the
+ * swimlanes. See RB.Taskboard.initialize()
+
+/* status labels */
+
+/* shared #impediments and #tasks */
+
+/* item styles used by .task and .impediment */
+
+/* dialog */
+
+.task_editor_dialog.ui-dialog
+ .ui-widget-header
+ background: none
+ background-color: #FFFFFF
+ opacity: 0.5
+ filter: alpha(opacity = 50)
+ .ui-dialog-title
+ float: right
+ margin-right: 0
+ color: $body-font-color
+ &.ui-widget-content
+ background: none
+ border: none
+ .editor
+ color: $body-font-color
+ .ui-dialog-buttonpane.ui-widget-content
+ background: none
+ background-color: none
+ border: none
+
+.ui-dialog
+ .ui-dialog-titlebar-close
+ .ui-button-text
+ display: none
+
+.dark #task_editor label
+ color: #FFFFFF
+
+.light #task_editor label
+ color: #000000
+
+.dark div
+ color: #FFFFFF
+
+.light div
+ color: #000000
+
+/* item editor */
+
+#task_editor
+ label
+ display: block
+ font-size: 11px
+ text-transform: capitalize
+ width: 100%
+ .editor
+ font-size: 11px
+ margin-bottom: 10px
+ width: 100%
+ .subject
+ height: 65px
+ width: 272px
+ .remaining_hours, .blocks
+ width: 268px
+
+/* compact view */
+
+#rb
+ .compact
+ .story, .label_sprint_impediments
+ height: 15px
+ .story .subject
+ display: none
+ .work_package
+ height: 21px
+ padding: 0
+ width: 21px
+ *
+ display: none
+ .placeholder
+ background-color: #FFFF00
+ border: 1px dashed #333300
+ height: 19px
+ width: 19px
+ #impediment_template, #task_template
+ display: none
+
+/* others */
+
+.story
+ .story-bar
+ @include story-header
+ text-align: right
+ width: 180px
+ clear: both
+ .id
+ float: right
+ .status
+ float: left
+
+.story,
+.label_sprint_impediments
+ font-size: 1rem - rem-calc(3px)
+
+.work_package
+ .id
+ @include story-header
+ text-align: right
+ width: 75px
+ a
+ opacity: 1.0
+ filter: alpha(opacity = 100)
+ .editable:hover
+ background-color: transparent
+ .subject.editable
+ font-size: 1rem - rem-calc(3px)
+ height: 42px
+ line-height: 13px
+ margin-top: 0
+ overflow: hidden
+ padding: 2px
+ width: 81px
+
+.story
+ .story-footer
+ .assigned_to_id
+ @include story-footer
+ @include ellipsis
+ .story-points
+ margin-top: 2px
+ float: right
+.work_package
+ .assigned_to_id.editable
+ @include story-footer
+ .t
+ @include ellipsis
+
+/* Toolbar modifications (no support for labels form the component) */
+
+#toolbar
+ label[for=col_width_input]
+ padding-top: rem-calc(20px)
+
+ #col_width_input
+ max-width: 60px
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_application_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_application_controller.rb
new file mode 100644
index 0000000000..a727c40f5a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_application_controller.rb
@@ -0,0 +1,67 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Base class of all controllers in Backlogs
+class RbApplicationController < ApplicationController
+ helper :rb_common
+
+ before_filter :load_sprint_and_project, :check_if_plugin_is_configured, :authorize
+
+ skip_before_action :verify_authenticity_token, if: -> { Rails.env.test? }
+
+ private
+
+ # Loads the project to be used by the authorize filter to determine if
+ # User.current has permission to invoke the method in question.
+ def load_sprint_and_project
+ # because of strong params, we want to pluck this variable out right now,
+ # otherwise it causes issues where we are doing `attributes=`.
+ if (@sprint_id = params.delete(:sprint_id))
+ @sprint = Sprint.find(@sprint_id)
+ @project = @sprint.project
+ end
+ # This overrides sprint's project if we set another project, say a subproject
+ @project = Project.find(params[:project_id]) if params[:project_id]
+ end
+
+ def check_if_plugin_is_configured
+ settings = Setting.plugin_openproject_backlogs
+ if settings['story_types'].blank? || settings['task_type'].blank?
+ respond_to do |format|
+ format.html { render file: 'shared/not_configured' }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_burndown_charts_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_burndown_charts_controller.rb
new file mode 100644
index 0000000000..7c130de47c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_burndown_charts_controller.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbBurndownChartsController < RbApplicationController
+ helper :burndown_charts
+
+ def show
+ @burndown = @sprint.burndown(@project)
+
+ respond_to do |format|
+ format.html { render layout: false }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_export_card_configurations_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_export_card_configurations_controller.rb
new file mode 100644
index 0000000000..00d1754358
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_export_card_configurations_controller.rb
@@ -0,0 +1,67 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbExportCardConfigurationsController < RbApplicationController
+ include OpenProject::PdfExport::ExportCard
+
+ before_filter :load_project_and_sprint
+
+ def index
+ @configs = ExportCardConfiguration.active
+ end
+
+ def show
+ config = ExportCardConfiguration.find(params[:id])
+
+ cards_document = OpenProject::PdfExport::ExportCard::DocumentGenerator.new(config, @sprint.stories(@project))
+
+ filename = "#{@project}-#{@sprint}-#{Time.now.strftime('%B-%d-%Y')}.pdf"
+ respond_to do |format|
+ format.pdf {
+ send_data(cards_document.render,
+ disposition: 'attachment',
+ type: 'application/pdf',
+ filename: filename)
+ }
+ end
+ end
+
+ private
+
+ def load_project_and_sprint
+ @project = Project.find(params[:project_id])
+ @sprint = Sprint.find(@sprint_id)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_impediments_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_impediments_controller.rb
new file mode 100644
index 0000000000..2980465726
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_impediments_controller.rb
@@ -0,0 +1,76 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbImpedimentsController < RbApplicationController
+ def create
+ @impediment = Impediment.create_with_relationships(impediment_params(Impediment.new), @project.id)
+ status = (@impediment.errors.empty? ? 200 : 400)
+ @include_meta = true
+
+ respond_to do |format|
+ format.html { render partial: 'impediment', object: @impediment, status: status }
+ end
+ end
+
+ def update
+ @impediment = Impediment.find(params[:id])
+ result = @impediment.update_with_relationships(impediment_params(@impediment))
+ status = (result ? 200 : 400)
+ @include_meta = true
+
+ respond_to do |format|
+ format.html { render partial: 'impediment', object: @impediment, status: status }
+ end
+ end
+
+private
+
+ def impediment_params(instance)
+ # We do not need project_id, since ApplicationController will take care of
+ # fetching the record.
+ params.delete(:project_id)
+
+ hash = params.permit(:fixed_version_id, :status_id, :id, :prev, :sprint_id,
+ :assigned_to_id, :remaining_hours, :subject, :blocks_ids)
+
+ # We block block_ids only when user is not allowed to create or update the
+ # instance passed.
+ unless instance && ((instance.new_record? && User.current.allowed_to?(:create_impediments, @project)) || User.current.allowed_to?(:update_impediments, @project))
+ hash.delete(:block_ids)
+ end
+
+ hash
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_master_backlogs_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_master_backlogs_controller.rb
new file mode 100644
index 0000000000..742a1cfe1f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_master_backlogs_controller.rb
@@ -0,0 +1,60 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbMasterBacklogsController < RbApplicationController
+ menu_item :backlogs
+
+ before_filter :set_export_card_config_meta
+
+ def index
+ @owner_backlogs = Backlog.owner_backlogs(@project)
+ @sprint_backlogs = Backlog.sprint_backlogs(@project)
+
+ @last_update = (@sprint_backlogs + @owner_backlogs).map(&:updated_on).compact.max
+ end
+
+ private
+
+ def set_export_card_config_meta
+ @export_card_config_meta = {
+ count: ExportCardConfiguration.active.count,
+ default: ExportCardConfiguration.default
+ }
+ end
+
+ def default_breadcrumb
+ l(:label_backlogs)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_queries_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_queries_controller.rb
new file mode 100644
index 0000000000..775a3afd47
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_queries_controller.rb
@@ -0,0 +1,59 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbQueriesController < RbApplicationController
+ include WorkPackagesFilterHelper
+
+ def show
+ filters = []
+ if @sprint_id
+ filters.push(filter_object('status_id', '*'))
+ filters.push(filter_object('fixed_version_id', '=', [@sprint_id]))
+ # Note: We need a filter for backlogs_work_package_type but currently it's not possible for plugins to introduce new filter types
+ else
+ filters.push(filter_object('status_id', 'o'))
+ filters.push(filter_object('fixed_version_id', '!*', [@sprint_id]))
+ # Same as above
+ end
+
+ query = {
+ f: filters,
+ c: ['type', 'status', 'priority', 'subject', 'assigned_to', 'updated_at', 'position'],
+ t: 'position:desc'
+ }
+
+ redirect_to project_work_packages_with_query_path(@project, query)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_sprints_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_sprints_controller.rb
new file mode 100644
index 0000000000..56ee9a00e5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_sprints_controller.rb
@@ -0,0 +1,62 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Responsible for exposing sprint CRUD. It SHOULD NOT be used for displaying the
+# taskboard since the taskboard is a management interface used for managing
+# objects within a sprint. For info about the taskboard, see
+# RbTaskboardsController
+class RbSprintsController < RbApplicationController
+ def update
+ result = @sprint.update_attributes(params.permit(:name,
+ :start_date,
+ :effective_date))
+ status = (result ? 200 : 400)
+
+ respond_to do |format|
+ format.html { render partial: 'sprint', status: status, object: @sprint }
+ end
+ end
+
+ # Overwrite load_sprint_and_project to load the sprint from the :id instead of
+ # :sprint_id
+ def load_sprint_and_project
+ if params[:id]
+ @sprint = Sprint.find(params[:id])
+ @project = @sprint.project
+ end
+ # This overrides sprint's project if we set another project, say a subproject
+ @project = Project.find(params[:project_id]) if params[:project_id]
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_stories_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_stories_controller.rb
new file mode 100644
index 0000000000..8cb9536d18
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_stories_controller.rb
@@ -0,0 +1,76 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbStoriesController < RbApplicationController
+ include OpenProject::PdfExport::ExportCard
+
+ # This is a constant here because we will recruit it elsewhere to whitelist
+ # attributes. This is necessary for now as we still directly use `attributes=`
+ # in non-controller code.
+ PERMITTED_PARAMS = [:id, :status_id, :fixed_version_id,
+ :story_points, :type_id, :subject, :author_id, :prev,
+ :sprint_id]
+
+ def create
+ params['author_id'] = User.current.id
+ prev = params.delete('prev')
+ story = Story.create_and_position(story_params, {project: @project,
+ author: User.current},
+ prev)
+ status = (story.id ? 200 : 400)
+
+ respond_to do |format|
+ format.html { render partial: 'story', object: story, status: status }
+ end
+ end
+
+ def update
+ story = Story.find(params[:id])
+ prev = params.delete('prev')
+ result = story.update_and_position!(story_params, @project, prev)
+ story.reload
+ status = (result ? 200 : 400)
+
+ respond_to do |format|
+ format.html { render partial: 'story', object: story, status: status }
+ end
+ end
+
+private
+
+ def story_params
+ params.permit(PERMITTED_PARAMS)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_taskboards_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_taskboards_controller.rb
new file mode 100644
index 0000000000..633ff12c5a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_taskboards_controller.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbTaskboardsController < RbApplicationController
+ menu_item :backlogs
+
+ helper :taskboards
+
+ def show
+ @statuses = Type.find(Task.type).statuses
+ @story_ids = @sprint.stories(@project).map(&:id)
+ @last_updated = Task.where(parent_id: @story_ids)
+ .order('updated_at DESC')
+ .first
+ end
+
+ def default_breadcrumb
+ l(:label_backlogs)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_tasks_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_tasks_controller.rb
new file mode 100644
index 0000000000..fe2677ed99
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_tasks_controller.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbTasksController < RbApplicationController
+
+ # This is a constant here because we will recruit it elsewhere to whitelist
+ # attributes. This is necessary for now as we still directly use `attributes=`
+ # in non-controller code.
+ PERMITTED_PARAMS = ["id", "subject", "assigned_to_id", "remaining_hours", "parent_id",
+ "estimated_hours", "status_id", "prev", "sprint_id"]
+
+ def create
+ @task = Task.create_with_relationships(task_params, @project.id)
+
+ status = (@task.errors.empty? ? 200 : 400)
+ @include_meta = true
+
+ respond_to do |format|
+ format.html { render partial: 'task', object: @task, status: status }
+ end
+ end
+
+ def update
+ @task = Task.find(task_params[:id])
+ result = @task.update_with_relationships(task_params)
+ status = (result ? 200 : 400)
+ @include_meta = true
+
+ respond_to do |format|
+ format.html { render partial: 'task', object: @task, status: status }
+ end
+ end
+
+private
+ def task_params
+ params.permit(PERMITTED_PARAMS)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/rb_wikis_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/rb_wikis_controller.rb
new file mode 100644
index 0000000000..5628b2cf61
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/rb_wikis_controller.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RbWikisController < RbApplicationController
+ # NOTE: The methods #show and #edit are public (see init.rb). We will let
+ # OpenProject's WikiController#index take care of autorization
+ #
+ # NOTE: The methods #show and #edit create a template page when called.
+ def show
+ redirect_to controller: '/wiki', action: 'index', project_id: @project.id, id: @sprint.wiki_page
+ end
+
+ def edit
+ redirect_to controller: '/wiki', action: 'edit', project_id: @project.id, id: @sprint.wiki_page
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/version_settings_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/version_settings_controller.rb
new file mode 100644
index 0000000000..fdbdca09fe
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/version_settings_controller.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class VersionSettingsController < RbApplicationController
+ def edit
+ @version = Version.find(params[:id])
+ end
+
+ private
+
+ def authorize
+ # Everyone with the right to edit versions has the right to edit version
+ # settings
+ super 'versions', 'edit'
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/controllers/work_package_boxes_controller.rb b/vendored-plugins/openproject-backlogs/app/controllers/work_package_boxes_controller.rb
new file mode 100644
index 0000000000..b1bb24c509
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/controllers/work_package_boxes_controller.rb
@@ -0,0 +1,91 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class WorkPackageBoxesController < WorkPackagesController
+ helper :rb_common
+
+ def show
+ return redirect_to work_package_path(params[:id]) unless request.xhr?
+
+ load_journals
+ @changesets = @work_package.changesets.visible.all
+ @changesets.reverse! if User.current.wants_comments_in_reverse_order?
+ @relations = @work_package.relations.select { |r| r.other_work_package(@work_package) && r.other_work_package(@work_package).visible? }
+ @allowed_statuses = @work_package.new_statuses_allowed_to(User.current)
+ @edit_allowed = User.current.allowed_to?(:edit_work_packages, @project)
+ @priorities = IssuePriority.all
+ @time_entry = TimeEntry.new
+
+ respond_to do |format|
+ format.js { render partial: 'show' }
+ end
+ end
+
+ def edit
+ return redirect_to edit_work_package_path(params[:id]) unless request.xhr?
+
+ update_work_package_from_params
+ load_journals
+ @journal = @work_package.current_journal
+
+ respond_to do |format|
+ format.js { render partial: 'edit' }
+ end
+ end
+
+ def update
+ update_work_package_from_params
+
+ if @work_package.save_work_package_with_child_records(params, @time_entry)
+ @work_package.reload
+ load_journals
+ respond_to do |format|
+ format.js { render partial: 'show' }
+ end
+ else
+ @journal = @work_package.current_journal
+ respond_to do |format|
+ format.js { render partial: 'edit' }
+ end
+ end
+ end
+
+ private
+
+ def load_journals
+ @journals = @work_package.journals.includes(:user).order("#{Journal.table_name}.created_at ASC")
+ @journals.reverse! if User.current.wants_comments_in_reverse_order?
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/helpers/burndown_charts_helper.rb b/vendored-plugins/openproject-backlogs/app/helpers/burndown_charts_helper.rb
new file mode 100644
index 0000000000..b350948d90
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/helpers/burndown_charts_helper.rb
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module BurndownChartsHelper
+ def yaxis_labels(burndown)
+ max = burndown.max[:points]
+
+ mvalue = (max / 25) + 1
+
+ labels = (0..mvalue).map { |i| "[#{i * 25}, #{i * 25}]" }
+
+ mvalue = mvalue + 1 if mvalue == 1 || ((max % 25) == 0)
+
+ labels << "[#{(mvalue) * 25}, '#{l('backlogs.points')} ']"
+
+ result = labels.join(', ')
+
+ result.html_safe
+ end
+
+ def xaxis_labels(burndown)
+ # 14 entries (plus the axis label) have come along as the best value for a good optical result.
+ # Thus it is enough space between the entries.
+ entries_displayed = (burndown.days.length / 14.0).ceil
+ result = burndown.days.enum_for(:each_with_index).map do |d, i|
+ if ((i % entries_displayed) == 0)
+ "[#{i + 1}, '#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])} #{d.strftime('%d/%m')}']"
+ end
+ end.join(',').html_safe +
+ ", [#{burndown.days.length + 1},
+ '#{I18n.t('backlogs.date')} ']".html_safe
+ end
+
+ def dataseries(burndown)
+ burndown.series.map { |s| "#{s.first}: {label: '#{l('backlogs.' + s.first.to_s)}', data: [#{s.last.enum_for(:each_with_index).map { |s, i| "[#{i + 1}, #{s}] " }.join(', ')}]} " }.join(', ').html_safe
+ end
+
+ def burndown_series_checkboxes(burndown)
+ boxes = ''
+ burndown.series(:all).map { |s| s.first.to_s }.sort.each do |series|
+ boxes += " #{l('backlogs.' + series.to_s)} "
+ end
+ boxes.html_safe
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/helpers/rb_common_helper.rb b/vendored-plugins/openproject-backlogs/app/helpers/rb_common_helper.rb
new file mode 100644
index 0000000000..f19022396f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/helpers/rb_common_helper.rb
@@ -0,0 +1,267 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module RbCommonHelper
+ def assignee_id_or_empty(story)
+ story.assigned_to_id.to_s
+ end
+
+ def assignee_name_or_empty(story)
+ story.blank? || story.assigned_to.blank? ? '' : "#{story.assigned_to.firstname} #{story.assigned_to.lastname}"
+ end
+
+ def blocks_ids(ids)
+ ids.sort.join(',')
+ end
+
+ def build_inline_style(task)
+ is_assigned_task?(task) ? color_style(task) : ''
+ end
+
+ def color_style(task)
+ background_color = get_backlogs_preference(task.assigned_to, :task_color)
+
+ "style=\"background-color:#{background_color};\"".html_safe
+ end
+
+ def color_contrast_class(task)
+ if is_assigned_task?(task)
+ color_contrast(background_color_hex(task)) ? 'light' : 'dark'
+ else
+ ''
+ end
+ end
+
+ def is_assigned_task?(task)
+ !(task.blank? || task.assigned_to.blank?)
+ end
+
+ def background_color_hex(task)
+ background_color = get_backlogs_preference(task.assigned_to, :task_color)
+ background_color_hex = background_color.sub(/\#/, '0x').hex
+ end
+
+ def id_or_empty(item)
+ item.id.to_s
+ end
+
+ def shortened_id(record)
+ id = record.id.to_s
+ (id.length > 8 ? "#{id[0..1]}...#{id[-4..-1]}" : id)
+ end
+
+ def work_package_link_or_empty(work_package)
+ modal_link_to_work_package(work_package.id, work_package, class: 'prevent_edit') unless work_package.new_record?
+ end
+
+ def modal_link_to_work_package(title, work_package, options = {})
+ modal_link_to(title, work_package_path(work_package), options)
+ end
+
+ def modal_link_to(title, path, options = {})
+ html_id = "modal_work_package_#{SecureRandom.hex(10)}"
+ link_to(title, path, options.merge(id: html_id, target: '_blank'))
+ end
+
+ def sprint_link_or_empty(item)
+ item_id = item.id.to_s
+ text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
+ item.new_record? ? '' : link_to(text, { controller: '/sprint', action: 'show', id: item }, class: 'prevent_edit')
+ end
+
+ def mark_if_closed(story)
+ !story.new_record? && work_package_status_for_id(story.status_id).is_closed? ? 'closed' : ''
+ end
+
+ def story_points_or_empty(story)
+ story.story_points.to_s
+ end
+
+ def status_id_or_default(story)
+ story.new_record? ? new_record_status.id : story.status_id
+ end
+
+ def status_label_or_default(story)
+ story.new_record? ? new_record_status.name : h(work_package_status_for_id(story.status_id).name)
+ end
+
+ def sprint_html_id_or_empty(sprint)
+ sprint.id.nil? ? '' : "sprint_#{sprint.id}"
+ end
+
+ def story_html_id_or_empty(story)
+ story.id.nil? ? '' : "story_#{story.id}"
+ end
+
+ def type_id_or_empty(story)
+ story.type_id.to_s
+ end
+
+ def type_name_or_empty(story)
+ story.type.nil? ? '' : h(backlogs_types_by_id[story.type_id].name)
+ end
+
+ def updated_on_with_milliseconds(story)
+ date_string_with_milliseconds(story.updated_on, 0.001) unless story.blank?
+ end
+
+ def date_string_with_milliseconds(d, add = 0)
+ return '' if d.blank?
+ d.strftime('%B %d, %Y %H:%M:%S') + '.' + (d.to_f % 1 + add).to_s.split('.')[1]
+ end
+
+ def remaining_hours(item)
+ item.remaining_hours.blank? || item.remaining_hours == 0 ? '' : item.remaining_hours
+ end
+
+ def available_story_types
+ @available_story_types ||= begin
+ types = story_types & @project.types if @project
+
+ types
+ end
+ end
+
+ def available_statuses_by_type
+ @available_statuses_by_type ||= begin
+ available_statuses_by_type = Hash.new do |type_hash, type|
+ type_hash[type] = Hash.new do |status_hash, status|
+ status_hash[status] = [status]
+ end
+ end
+
+ workflows = all_workflows
+
+ workflows.each do |w|
+ type_status = available_statuses_by_type[story_types_by_id[w.type_id]][w.old_status]
+
+ type_status << w.new_status unless type_status.include?(w.new_status)
+ end
+
+ available_statuses_by_type
+ end
+ end
+
+ def show_burndown_link(sprint)
+ ret = ''
+
+ ret += link_to(l('backlogs.show_burndown_chart'),
+ {},
+ class: 'show_burndown_chart button')
+
+ ret += javascript_tag "
+ jQuery(document).ready(function(){
+ var burndown = RB.Factory.initialize(RB.Burndown, jQuery('.show_burndown_chart'));
+ burndown.setSprintId(#{sprint.id});
+ });"
+ ret.html_safe
+ end
+
+ private
+
+ def new_record_status
+ @new_record_status ||= all_work_package_status.first
+ end
+
+ def default_work_package_status
+ @default_work_package_status ||= all_work_package_status.detect(&:is_default)
+ end
+
+ def work_package_status_for_id(id)
+ @all_work_package_status_by_id ||= begin
+ all_work_package_status.inject({}) do |mem, status|
+ mem[status.id] = status
+ mem
+ end
+ end
+
+ @all_work_package_status_by_id[id]
+ end
+
+ def all_workflows
+ @all_workflows ||= Workflow.includes([:new_status, :old_status])
+ .where(role_id: User.current.roles_for_project(@project).map(&:id),
+ type_id: story_types.map(&:id))
+ end
+
+ def all_work_package_status
+ @all_work_package_status ||= Status.order('position ASC')
+ end
+
+ def backlogs_types
+ @backlogs_types ||= begin
+ backlogs_ids = Setting.plugin_openproject_backlogs['story_types']
+ backlogs_ids << Setting.plugin_openproject_backlogs['task_type']
+
+ Type.where(id: backlogs_ids).order('position ASC')
+ end
+ end
+
+ def backlogs_types_by_id
+ @backlogs_types_by_id ||= begin
+ backlogs_types.inject({}) do |mem, type|
+ mem[type.id] = type
+ mem
+ end
+ end
+ end
+
+ def story_types
+ @story_types ||= begin
+ backlogs_type_ids = Setting.plugin_openproject_backlogs['story_types'].map(&:to_i)
+
+ backlogs_types.select { |t| backlogs_type_ids.include?(t.id) }
+ end
+ end
+
+ def story_types_by_id
+ @story_types_by_id ||= begin
+ story_types.inject({}) do |mem, type|
+ mem[type.id] = type
+ mem
+ end
+ end
+ end
+
+ def get_backlogs_preference(assignee, attr)
+ assignee.is_a?(User) ? assignee.backlogs_preference(attr) : '#24B3E7'
+ end
+
+ def template_story
+ Story.new.tap do |s|
+ s.type = available_story_types.first
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/helpers/rb_master_backlogs_helper.rb b/vendored-plugins/openproject-backlogs/app/helpers/rb_master_backlogs_helper.rb
new file mode 100644
index 0000000000..3648c03471
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/helpers/rb_master_backlogs_helper.rb
@@ -0,0 +1,146 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module RbMasterBacklogsHelper
+ include Redmine::I18n
+
+ def render_backlog_menu(backlog)
+ content_tag(:div, class: 'menu') do
+ [
+ content_tag(:div, '', class: 'ui-icon ui-icon-carat-1-s'),
+ content_tag(:ul, class: 'items') do
+ backlog_menu_items_for(backlog).map { |item|
+ content_tag(:li, item, class: 'item')
+ }.join.html_safe
+ end
+ ].join.html_safe
+ end
+ end
+
+ def backlog_menu_items_for(backlog)
+ items = common_backlog_menu_items_for(backlog)
+
+ if backlog.sprint_backlog?
+ items.merge!(sprint_backlog_menu_items_for(backlog))
+ end
+
+ menu = []
+ [:new_story, :stories_tasks, :task_board, :burndown, :cards, :wiki, :configs, :properties].each do |key|
+ menu << items[key] if items.keys.include?(key)
+ end
+
+ menu
+ end
+
+ def common_backlog_menu_items_for(backlog)
+ items = {}
+
+ items[:new_story] = content_tag(:a,
+ l('backlogs.add_new_story'),
+ href: '#',
+ class: 'add_new_story')
+
+ items[:stories_tasks] = link_to(l(:label_stories_tasks),
+ controller: '/rb_queries',
+ action: 'show',
+ project_id: @project,
+ sprint_id: backlog.sprint)
+
+ if @export_card_config_meta[:count] > 0
+ items[:configs] = export_export_cards_link(backlog)
+ end
+
+ if current_user.allowed_to?(:manage_versions, @project)
+ items[:properties] = properties_link(backlog)
+ end
+
+ items
+ end
+
+ def export_export_cards_link(backlog)
+ if @export_card_config_meta[:count] == 1
+ link_to(l(:label_backlogs_export_card_export),
+ controller: '/rb_export_card_configurations',
+ action: 'show',
+ project_id: @project,
+ sprint_id: backlog.sprint,
+ id: @export_card_config_meta[:default],
+ format: :pdf)
+ else
+ export_modal_link(backlog)
+ end
+ end
+
+ def properties_link(backlog)
+ back_path = backlogs_project_backlogs_path(@project)
+
+ version_path = edit_version_path(backlog.sprint, back_url: back_path)
+
+ link_to(l(:'backlogs.properties'), version_path)
+ end
+
+ def export_modal_link(backlog, options = {})
+ path = backlogs_project_sprint_export_card_configurations_path(@project.id, backlog.sprint.id)
+ html_id = "modal_work_package_#{SecureRandom.hex(10)}"
+ link_to(l(:label_backlogs_export_card_export), path, options.merge(id: html_id, :'data-modal' => ''))
+ end
+
+ def sprint_backlog_menu_items_for(backlog)
+ items = {}
+
+ items[:task_board] = link_to(l(:label_task_board),
+ controller: '/rb_taskboards',
+ action: 'show',
+ project_id: @project,
+ sprint_id: backlog.sprint)
+
+ if backlog.sprint.has_burndown?
+ items[:burndown] = content_tag(:a,
+ l('backlogs.show_burndown_chart'),
+ href: '#',
+ class: 'show_burndown_chart')
+ end
+
+ if @project.module_enabled? 'wiki'
+ items[:wiki] = link_to(l(:label_wiki),
+ controller: '/rb_wikis',
+ action: 'edit',
+ project_id: @project,
+ sprint_id: backlog.sprint)
+ end
+
+ items
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/helpers/taskboards_helper.rb b/vendored-plugins/openproject-backlogs/app/helpers/taskboards_helper.rb
new file mode 100644
index 0000000000..b8db47ab81
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/helpers/taskboards_helper.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module TaskboardsHelper
+ def impediments_by_position_for_status(sprint, project, status)
+ sprint.impediments(project).select { |i| i.status_id == status.id }.sort_by { |i| i.position.present? ? i.position : 0 }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/helpers/version_settings_helper.rb b/vendored-plugins/openproject-backlogs/app/helpers/version_settings_helper.rb
new file mode 100644
index 0000000000..a59b514098
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/helpers/version_settings_helper.rb
@@ -0,0 +1,87 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module VersionSettingsHelper
+ def version_settings_fields(version, project)
+ setting = version_setting_for_project(version, project)
+
+ content_tag :div, class: 'form--field' do
+ [
+ styled_label_tag(name_for_setting_attributes('display'), l(:label_column_in_backlog)),
+ styled_select_tag(name_for_setting_attributes('display'), options_for_select(position_display_options, setting.display)),
+ version_hidden_id_field(setting),
+ hidden_field_tag('project_id', project.id)
+ ].join.html_safe
+ end
+ end
+
+ private
+
+ def version_hidden_id_field(setting)
+ return '' unless setting.id
+ hidden_field_tag(name_for_setting_attributes('id'), setting.id)
+ end
+
+ def version_setting_for_project(version, project)
+ setting = version.version_settings.detect { |vs| vs.project_id == project.id || vs.project_id.nil? }
+
+ # nil? because some settings in the active codebase do have that right now
+ setting ||= version.version_settings.new(display: VersionSetting::DISPLAY_LEFT, project: project)
+
+ setting
+ end
+
+ def name_for_setting_attributes(attribute)
+ "version[version_settings_attributes][][#{attribute}]"
+ end
+
+ def position_display_options
+ options = [::VersionSetting::DISPLAY_NONE,
+ ::VersionSetting::DISPLAY_LEFT,
+ ::VersionSetting::DISPLAY_RIGHT]
+ options.map { |s| [humanize_display_option(s), s] }
+ end
+
+ def humanize_display_option(option)
+ case option
+ when ::VersionSetting::DISPLAY_NONE
+ t('version_settings_display_option_none')
+ when ::VersionSetting::DISPLAY_LEFT
+ t('version_settings_display_option_left')
+ when ::VersionSetting::DISPLAY_RIGHT
+ t('version_settings_display_option_right')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/backlog.rb b/vendored-plugins/openproject-backlogs/app/models/backlog.rb
new file mode 100644
index 0000000000..5f6cc010ea
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/backlog.rb
@@ -0,0 +1,76 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Backlog
+ attr_accessor :sprint
+ attr_accessor :stories
+
+ def self.owner_backlogs(project, options = {})
+ options.reverse_merge!(limit: nil)
+
+ backlogs = Sprint.apply_to(project).open.displayed_right(project).order_by_name
+
+ stories_by_sprints = Story.backlogs(project.id, backlogs.map(&:id))
+
+ backlogs.map { |sprint| new(stories: stories_by_sprints[sprint.id], owner_backlog: true, sprint: sprint) }
+ end
+
+ def self.sprint_backlogs(project)
+ sprints = Sprint.apply_to(project).open.displayed_left(project).order_by_date
+
+ stories_by_sprints = Story.backlogs(project.id, sprints.map(&:id))
+
+ sprints.map { |sprint| new(stories: stories_by_sprints[sprint.id], sprint: sprint) }
+ end
+
+ def initialize(options = {})
+ options = options.with_indifferent_access
+ @sprint = options['sprint']
+ @stories = options['stories']
+ @owner_backlog = options['owner_backlog']
+ end
+
+ def updated_on
+ @stories.max_by(&:updated_at).try(:updated_at)
+ end
+
+ def owner_backlog?
+ !!@owner_backlog
+ end
+
+ def sprint_backlog?
+ !owner_backlog?
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/burndown.rb b/vendored-plugins/openproject-backlogs/app/models/burndown.rb
new file mode 100644
index 0000000000..9b6379c2cc
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/burndown.rb
@@ -0,0 +1,123 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Burndown
+ def initialize(sprint, project, burn_direction = nil)
+ burn_direction ||= Setting.plugin_openproject_backlogs['points_burn_direction']
+
+ @sprint_id = sprint.id
+
+ make_date_series sprint
+
+ series_data = OpenProject::Backlogs::Burndown::SeriesRawData.new(project,
+ sprint,
+ points: ['story_points'])
+
+ series_data.collect_data
+
+ calculate_series series_data
+
+ determine_max
+ end
+
+ attr_reader :days
+ attr_reader :sprint_id
+ attr_reader :max
+
+ attr_reader :story_points
+ attr_reader :story_points_ideal
+
+ def series(_select = :active)
+ @available_series
+ end
+
+ private
+
+ def make_date_series(sprint)
+ @days = sprint.days
+ end
+
+ def calculate_series(series_data)
+ series_data.collect_names.each do |c|
+ # need to differentiate between hours and sp
+ make_series c.to_sym, series_data.unit_for(c), series_data[c].to_a.sort_by(&:first).map(&:last)
+ end
+
+ calculate_ideals(series_data)
+ end
+
+ def calculate_ideals(data)
+ (['story_points'] & data.collect_names).each do |ideal|
+ calculate_ideal(ideal, data.unit_for(ideal))
+ end
+ end
+
+ def calculate_ideal(name, unit)
+ max = send(name).first || 0.0
+ delta = max / (days.size - 1)
+
+ ideal = []
+ days.each_with_index do |_d, i|
+ ideal[i] = max - delta * i
+ end
+
+ make_series name.to_s + '_ideal', unit, ideal
+ end
+
+ def make_series(name, units, data)
+ @available_series ||= {}
+ s = OpenProject::Backlogs::Burndown::Series.new(data, name, units)
+ @available_series[name] = s
+ instance_variable_set("@#{name}", s)
+ end
+
+ def determine_max
+ @max = {
+ points: @available_series.values.select { |s| s.unit == :points }.flatten.compact.reject(&:nan?).max || 0.0,
+ hours: @available_series.values.select { |s| s.unit == :hours }.flatten.compact.reject(&:nan?).max || 0.0
+ }
+ end
+
+ def to_h(keys, values)
+ Hash[*keys.zip(values).flatten]
+ end
+
+ def workday_before(date = Date.today)
+ d = date - 1
+ # TODO: make weekday configurable
+ d = workday_before(d) unless d.wday > 0 and d.wday < 6
+ d
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/impediment.rb b/vendored-plugins/openproject-backlogs/app/models/impediment.rb
new file mode 100644
index 0000000000..b828dd41ac
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/impediment.rb
@@ -0,0 +1,96 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Impediment < Task
+ extend OpenProject::Backlogs::Mixins::PreventIssueSti
+
+ after_save :update_blocks_list
+
+ validate :validate_blocks_list
+
+ def self.default_scope
+ where(parent_id: nil, type_id: type)
+ end
+
+ def blocks_ids=(ids)
+ @blocks_ids_list = [ids] if ids.is_a?(Integer)
+ @blocks_ids_list = ids.split(/\D+/).map(&:to_i) if ids.is_a?(String)
+ @blocks_ids_list = ids.map(&:to_i) if ids.is_a?(Array)
+ end
+
+ def blocks_ids
+ @blocks_ids_list ||= relations_from.select { |rel| rel.relation_type == Relation::TYPE_BLOCKS }.map(&:to_id)
+ end
+
+ def self.create_with_relationships(params, project_id)
+ create_with_relationships_without_move(params, project_id)
+ end
+
+
+ def update_with_relationships(params, _is_impediment = false)
+ update_with_relationships_without_move(params)
+ end
+
+ private
+
+ def update_blocks_list
+ relations_from = [] if relations_from.nil?
+ remove_from_blocks_list
+ add_to_blocks_list
+ end
+
+ def remove_from_blocks_list
+ relations_from.delete(relations_from.select { |rel| rel.relation_type == Relation::TYPE_BLOCKS && !blocks_ids.include?(rel.to_id) })
+ end
+
+ def add_to_blocks_list
+ currently_blocking = relations_from.select { |rel| rel.relation_type == Relation::TYPE_BLOCKS }.map(&:to_id)
+
+ (blocks_ids - currently_blocking).each{ |id|
+ rel = Relation.new(relation_type: Relation::TYPE_BLOCKS, from: self)
+ rel.to_id = id
+ relations_from << rel
+ }
+ end
+
+ def validate_blocks_list
+ if blocks_ids.size == 0
+ errors.add :blocks_ids, :must_block_at_least_one_work_package
+ else
+ work_packages = WorkPackage.where(id: blocks_ids)
+ errors.add :blocks_ids, :can_only_contain_work_packages_of_current_sprint if work_packages.size == 0 || work_packages.any? { |i| i.fixed_version != fixed_version }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/sprint.rb b/vendored-plugins/openproject-backlogs/app/models/sprint.rb
new file mode 100644
index 0000000000..2b63f826c7
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/sprint.rb
@@ -0,0 +1,160 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'date'
+
+class Sprint < Version
+ scope :open_sprints, lambda { |project|
+ where(["versions.status = 'open' and versions.project_id = ?", project.id])
+ .order("COALESCE(start_date, CAST('4000-12-30' as date)) ASC, COALESCE(effective_date, CAST('4000-12-30' as date)) ASC")
+ }
+
+ # null last ordering
+ scope :order_by_date, -> {
+ order "COALESCE(start_date, CAST('4000-12-30' as date)) ASC, COALESCE(effective_date, CAST('4000-12-30' as date)) ASC"
+ }
+ scope :order_by_name, -> {
+ order "#{Version.table_name}.name ASC"
+ }
+
+ scope :apply_to, lambda { |project|
+ where("#{Version.table_name}.project_id = #{project.id}" +
+ " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
+ " #{Version.table_name}.sharing = 'system'" +
+ " OR (#{Project.table_name}.lft >= #{project.root.lft} AND #{Project.table_name}.rgt <= #{project.root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
+ " OR (#{Project.table_name}.lft < #{project.lft} AND #{Project.table_name}.rgt > #{project.rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
+ " OR (#{Project.table_name}.lft > #{project.lft} AND #{Project.table_name}.rgt < #{project.rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
+ '))')
+ .includes(:project)
+ .references(:projects)
+ }
+
+ scope :displayed_left, lambda { |project|
+ joins(sanitize_sql_array(["LEFT OUTER JOIN (SELECT * from #{VersionSetting.table_name}" +
+ ' WHERE project_id = ? ) version_settings' +
+ ' ON version_settings.version_id = versions.id',
+ project.id]))
+ .where(['(version_settings.project_id = ? AND version_settings.display = ?)' +
+ ' OR (version_settings.project_id is NULL)',
+ project.id, VersionSetting::DISPLAY_LEFT])
+ }
+
+ scope :displayed_right, lambda { |project|
+ where(['version_settings.project_id = ? AND version_settings.display = ?',
+ project.id, VersionSetting::DISPLAY_RIGHT])
+ .includes(:version_settings)
+ .references(:version_settings)
+ }
+
+ def stories(project, options = {})
+ Story.sprint_backlog(project, self, options)
+ end
+
+ def points
+ stories.inject(0) { |sum, story| sum + story.story_points.to_i }
+ end
+
+ def has_wiki_page
+ return false if wiki_page_title.blank?
+
+ page = project.wiki.find_page(wiki_page_title)
+ return false if !page
+
+ template = project.wiki.find_page(Setting.plugin_openproject_backlogs['wiki_template'])
+ return false if template && page.text == template.text
+
+ true
+ end
+
+ def wiki_page
+ return '' unless project.wiki
+
+ update_attribute(:wiki_page_title, Wiki.titleize(name)) if wiki_page_title.blank?
+
+ page = project.wiki.find_page(wiki_page_title)
+ template = project.wiki.find_page(Setting.plugin_openproject_backlogs['wiki_template'])
+
+ if template and not page
+ page = project.wiki.pages.build(title: wiki_page_title)
+ page.build_content(text: "h1. #{name}\n\n#{template.text}")
+ page.save!
+ end
+
+ wiki_page_title
+ end
+
+ def days(cutoff = nil, alldays = false)
+ # TODO: Assumes mon-fri are working days, sat-sun are not. This assumption
+ # is not globally right, we need to make this configurable.
+ cutoff = effective_date if cutoff.nil?
+
+ (start_date..cutoff).select { |d| alldays || (d.wday > 0 and d.wday < 6) }
+ end
+
+ def has_burndown?
+ !!(effective_date and start_date)
+ end
+
+ def activity
+ bd = burndown('up')
+ return false if bd.blank?
+
+ # Assume a sprint is active if it's only 2 days old
+ return true if bd.remaining_hours.size <= 2
+
+ WorkPackage.exists?(['fixed_version_id = ? and ((updated_on between ? and ?) or (created_on between ? and ?))',
+ id, -2.days.from_now, Time.now, -2.days.from_now, Time.now])
+ end
+
+ def burndown(project, burn_direction = nil)
+ return nil unless self.has_burndown?
+
+ @cached_burndown ||= Burndown.new(self, project, burn_direction)
+ end
+
+ def self.generate_burndown(only_current = true)
+ if only_current
+ conditions = ['? BETWEEN start_date AND effective_date', Date.today]
+ else
+ conditions = '1 = 1'
+ end
+
+ Version.where(conditions).each(&:burndown)
+ end
+
+ def impediments(project)
+ Impediment.where(fixed_version_id: self, project_id: project)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/story.rb b/vendored-plugins/openproject-backlogs/app/models/story.rb
new file mode 100644
index 0000000000..64d3cb0764
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/story.rb
@@ -0,0 +1,192 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Story < WorkPackage
+ extend OpenProject::Backlogs::Mixins::PreventIssueSti
+
+ def self.backlogs(project_id, sprint_ids, options = {})
+ options.reverse_merge!(order: Story::ORDER,
+ conditions: Story.condition(project_id, sprint_ids))
+
+ candidates = Story.where(options[:conditions]).order(options[:order])
+
+ stories_by_version = Hash.new do |hash, sprint_id|
+ hash[sprint_id] = []
+ end
+
+ candidates.each do |story|
+ last_rank = stories_by_version[story.fixed_version_id].size > 0 ?
+ stories_by_version[story.fixed_version_id].last.rank :
+ 0
+
+ story.rank = last_rank + 1
+ stories_by_version[story.fixed_version_id] << story
+ end
+
+ stories_by_version
+ end
+
+ def self.sprint_backlog(project, sprint, options = {})
+ Story.backlogs(project.id, [sprint.id], options)[sprint.id]
+ end
+
+ def self.create_and_position(params, safer_attributes, prev)
+ Story.new.tap do |s|
+ s.author = safer_attributes[:author] if safer_attributes[:author]
+ s.project = safer_attributes[:project] if safer_attributes[:project]
+ s.attributes = params
+
+ if s.save
+ s.move_after(prev)
+ end
+ end
+ end
+
+ def self.at_rank(project_id, sprint_id, rank)
+ Story.where(Story.condition(project_id, sprint_id))
+ .joins(:status)
+ .order(Story::ORDER)
+ .offset(rank -1)
+ .first
+ end
+
+ def self.types
+ types = Setting.plugin_openproject_backlogs['story_types']
+ return [] if types.blank?
+
+ types.map { |type| Integer(type) }
+ end
+
+ def tasks
+ Task.tasks_for(id)
+ end
+
+ def tasks_and_subtasks
+ return [] unless Task.type
+ descendants.where(type_id: Task.type)
+ end
+
+ def direct_tasks_and_subtasks
+ return [] unless Task.type
+ children.where(type_id: Task.type).map { |t| [t] + t.descendants }.flatten
+ end
+
+ def set_points(p)
+ init_journal(User.current)
+
+ if p.blank? || p == '-'
+ update_attribute(:story_points, nil)
+ return
+ end
+
+ if p.downcase == 's'
+ update_attribute(:story_points, 0)
+ return
+ end
+
+ p = Integer(p)
+ if p >= 0
+ update_attribute(:story_points, p)
+ return
+ end
+ end
+
+ # TODO: Refactor and add tests
+ #
+ # groups = tasks.partion(&:closed?)
+ # {:open => tasks.last.size, :closed => tasks.first.size}
+ #
+ def task_status
+ closed = 0
+ open = 0
+
+ tasks.each do |task|
+ if task.closed?
+ closed += 1
+ else
+ open += 1
+ end
+ end
+
+ { open: open, closed: closed }
+ end
+
+ def update_and_position!(params, project, prev)
+ self.attributes = params
+ self.project = project
+ self.status_id = nil if params[:status_id] == ''
+
+ save.tap do |result|
+ if result and prev
+ reload
+ move_after(prev)
+ end
+ end
+ end
+
+ def rank=(r)
+ @rank = r
+ end
+
+ def rank
+ if position.blank?
+ extras = ["and ((#{WorkPackage.table_name}.position is NULL and #{WorkPackage.table_name}.id <= ?) or not #{WorkPackage.table_name}.position is NULL)", id]
+ else
+ extras = ["and not #{WorkPackage.table_name}.position is NULL and #{WorkPackage.table_name}.position <= ?", position]
+ end
+
+ @rank ||= WorkPackage.where(Story.condition(project.id, fixed_version_id, extras))
+ .joins(:status)
+ .count
+ @rank
+ end
+
+ private
+
+ def self.condition(project_id, sprint_ids, extras = [])
+ c = ['project_id = ? AND type_id in (?) AND fixed_version_id in (?)',
+ project_id, Story.types, sprint_ids]
+
+ if extras.size > 0
+ c[0] += ' ' + extras.shift
+ c += extras
+ end
+
+ c
+ end
+
+ # This forces NULLS-LAST ordering
+ ORDER = "CASE WHEN #{WorkPackage.table_name}.position IS NULL THEN 1 ELSE 0 END ASC, CASE WHEN #{WorkPackage.table_name}.position IS NULL THEN #{WorkPackage.table_name}.id ELSE #{WorkPackage.table_name}.position END ASC"
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/task.rb b/vendored-plugins/openproject-backlogs/app/models/task.rb
new file mode 100644
index 0000000000..fb7f722ff0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/task.rb
@@ -0,0 +1,121 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'date'
+
+class Task < WorkPackage
+ extend OpenProject::Backlogs::Mixins::PreventIssueSti
+
+ def self.type
+ task_type = Setting.plugin_openproject_backlogs['task_type']
+ task_type.blank? ? nil : task_type.to_i
+ end
+
+ # This method is used by Backlogs::List. It ensures, that tasks and stories
+ # follow a similar interface
+ def self.types
+ [type]
+ end
+
+ def self.create_with_relationships(params, project_id)
+ prev = params.delete(:prev)
+ if task = create_with_relationships_without_move(params, project_id)
+ task.move_after prev
+ end
+
+ task
+ end
+
+ def self.tasks_for(story_id)
+ Task.where(parent_id: story_id).order(:lft).each_with_index do |task, i|
+ task.rank = i + 1
+ end
+ end
+
+ def status_id=(id)
+ super
+ self.remaining_hours = 0 if Status.find(id).is_closed?
+ end
+
+ def update_with_relationships(params, _is_impediment = false)
+ prev = params.delete(:prev)
+ update_with_relationships_without_move(params).tap do |result|
+ move_after(prev) if result
+ end
+ end
+
+ # Assumes the task is already under the same story as 'prev_id'
+ def move_after(prev_id)
+ if prev_id.blank?
+ sib = siblings
+ move_to_left_of(sib[0].id) if sib.any?
+ else
+ move_to_right_of(prev_id)
+ end
+ end
+
+ def rank=(r)
+ @rank = r
+ end
+
+ def rank
+ @rank ||= WorkPackage.where(['type_id = ? and not parent_id is NULL and root_id = ? and lft <= ?', Task.type, story_id, lft]).count
+ @rank
+ end
+
+ def self.create_with_relationships_without_move(params, project_id)
+ # Important, otherwise attributes= fails with unknown attribute error.
+ _prev = params.delete(:prev)
+
+ task = new
+
+ task.author = User.current
+ task.project_id = project_id
+ task.type_id = Task.type
+
+ task.attributes = params
+
+ task.save
+
+ task
+ end
+
+ def update_with_relationships_without_move(params)
+ prev = params.delete(:prev)
+ self.attributes = params
+
+ save
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/models/version_setting.rb b/vendored-plugins/openproject-backlogs/app/models/version_setting.rb
new file mode 100644
index 0000000000..7562040efd
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/models/version_setting.rb
@@ -0,0 +1,69 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class VersionSetting < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :version
+
+ validates_presence_of :project
+
+ DISPLAY_NONE = 1
+ DISPLAY_LEFT = 2
+ DISPLAY_RIGHT = 3
+
+ def display_right?
+ display == DISPLAY_RIGHT
+ end
+
+ def display_right!
+ self.display = DISPLAY_RIGHT
+ end
+
+ def display_left?
+ display == DISPLAY_LEFT
+ end
+
+ def display_left!
+ self.display = DISPLAY_LEFT
+ end
+
+ def display_none?
+ display == DISPLAY_NONE
+ end
+
+ def display_none!
+ self.display = DISPLAY_NONE
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/app/views/hooks/backlogs/_view_work_packages_form_details_bottom.html.erb b/vendored-plugins/openproject-backlogs/app/views/hooks/backlogs/_view_work_packages_form_details_bottom.html.erb
new file mode 100644
index 0000000000..5eb122010b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/hooks/backlogs/_view_work_packages_form_details_bottom.html.erb
@@ -0,0 +1,90 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% work_package = issue %>
+
+<% if work_package.is_story? && params[:copy_from] %>
+
+
+ <%= styled_label_tag :link_to_original, l(:rb_label_link_to_original) %>
+
+ <%= styled_check_box_tag('link_to_original',
+ params[:copy_from],
+ true, label: l(:rb_label_link_to_original)) %>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/projects/settings/_backlogs_settings.html.erb b/vendored-plugins/openproject-backlogs/app/views/projects/settings/_backlogs_settings.html.erb
new file mode 100644
index 0000000000..38b973a5e8
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/projects/settings/_backlogs_settings.html.erb
@@ -0,0 +1,99 @@
+<%#-- copyright
+OpenProject is a project management system.
+Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% statuses_done_for_project = @project.done_statuses.select(:id).map(&:id) %>
+
+<%= styled_form_tag(controller: '/projects', action: "project_done_statuses", id: @project) do %>
+
+
+ <%=l('backlogs.definition_of_done')%>
+
+
+ (<%= check_all_links 'form--backlogs' %>)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% for status in @statuses %>
+
+
+ <%= status.name %>
+
+
+ <% checkbox_id = status.name.parameterize.underscore %>
+ <%= styled_label_tag checkbox_id, l('backlogs.label_is_done_status', status_name: status.name), class: 'hidden-for-sighted' %>
+ <%= (styled_check_box_tag 'statuses[][status_id]', status.id.to_s, statuses_done_for_project.include?(status.id), id: checkbox_id) %>
+
+
+ <% end %>
+
+
+
+
+
+
+ <%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
+
+
+
+<% end %>
+
+<%=l('backlogs.rebuild_positions')%>
+
+<%= styled_form_tag(:controller => '/projects', :action => "rebuild_positions", :id => @project) do %>
+ <%= styled_button_tag l('backlogs.rebuild'), class: '-highlight' %>
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/projects/settings/_versions.html.erb b/vendored-plugins/openproject-backlogs/app/views/projects/settings/_versions.html.erb
new file mode 100644
index 0000000000..719d6a1d45
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/projects/settings/_versions.html.erb
@@ -0,0 +1,177 @@
+<%#-- copyright
+OpenProject is a project management system.
+Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+<% if @project.shared_versions.any? %>
+
+
+ <%= l(:label_available_project_versions) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% for version in @project.shared_versions.sort %>
+
+
+ <%= link_to_version version %>
+
+ <%= format_date(version.start_date) %>
+ <%= format_date(version.effective_date) %>
+ <%=h version.description %>
+ <%= l("version_status_#{version.status}") %>
+ <%=h format_version_sharing(version.sharing) %>
+
+ <%= link_to_if_authorized( h(version.wiki_page_title),
+ { controller: '/wiki',
+ action: 'show',
+ project_id: version.project,
+ id: Wiki.titleize(version.wiki_page_title) }) ||
+ h(version.wiki_page_title) unless version.wiki_page_title.blank? || version.project.wiki.nil? %>
+
+
+ <% if version.project == @project %>
+ <%= link_to_if_authorized l(:button_edit),
+ { controller: '/versions', action: 'edit', id: version },
+ class: 'icon icon-edit' %>
+ <%= link_to_if_authorized l(:button_delete),
+ { controller: '/versions', action: 'destroy', id: version },
+ data: { confirm: l(:text_are_you_sure) },
+ method: :delete,
+ class: 'icon icon-delete' %>
+ <% elsif @project.enabled_modules.collect(&:name).include?("backlogs") %>
+ <%= link_to_if_authorized l(:button_edit),
+ { :controller => '/versions', :action => 'edit', :id => version, :project_id => @project.id },
+ :class => 'icon icon-edit' %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+ <% if @project.versions.any? %>
+
+ <%= link_to_if_authorized({ controller: '/versions', action: 'new', project_id: @project },
+ class: 'button -alt-highlight') do %>
+
+ <%= l(:label_version_new) %>
+ <% end %>
+
+ <% end %>
+ <% else %>
+ <%= no_results_box(action_url: new_project_version_path(@project), display_action: authorize_for('versions', 'new')) %>
+ <% end %>
+
+
+<% if @project.versions.any? %>
+
+ <%= link_to l(:label_close_versions),
+ close_completed_project_versions_path(@project),
+ method: :put %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/_burndown.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/_burndown.html.erb
new file mode 100644
index 0000000000..5857a0d45b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/_burndown.html.erb
@@ -0,0 +1,136 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= javascript_include_tag 'backlogs/jquery.flot/excanvas.js' %>
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/show.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/show.html.erb
new file mode 100644
index 0000000000..c0725d78ce
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_burndown_charts/show.html.erb
@@ -0,0 +1,52 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+ <%= "#{@sprint.name}: #{@sprint.start_date.present? ? I18n.l(@sprint.start_date) : ''} - #{@sprint.effective_date.present? ? I18n.l(@sprint.effective_date) : ''}" %>
+
+
+<% if @burndown %>
+ <%= render :partial => 'burndown', :locals => {:div => 'burndown_', :burndown => @burndown } %>
+
+ <%=l('backlogs.generating_chart')%>
+
+
+ <%= l('backlogs.chart_options') %>
+ <%= burndown_series_checkboxes(@burndown) %>
+
+<% else %>
+ <%= l('backlogs.no_burndown_data')%>
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_export_card_configurations/index.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_export_card_configurations/index.html.erb
new file mode 100644
index 0000000000..061526af3d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_export_card_configurations/index.html.erb
@@ -0,0 +1,58 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+
+<%= toolbar title: l(:label_backlogs_export_card_config_select) %>
+
+
+<% @configs.each do |config| %>
+
+
+ <%= link_to(config.name,
+ :controller => '/rb_export_card_configurations',
+ :action => 'show',
+ :project_id => @project.id,
+ :sprint_id => @sprint.id,
+ :id => config.id,
+ :format => :pdf) %>
+ <% if !!config.description && !config.description.empty? %>
+ <%= " - #{config.description}" %>
+ <% end %>
+
+
+<% end %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_impediments/_impediment.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_impediments/_impediment.html.erb
new file mode 100644
index 0000000000..5b44af6bfd
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_impediments/_impediment.html.erb
@@ -0,0 +1,56 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% prevent_edit = User.current.allowed_to?(:update_impediments, defined?(project) ? project : impediment.project) ? '' : 'prevent_edit'%>
+>
+
+
<%= work_package_link_or_empty(impediment) %>
+
<%= id_or_empty(impediment) %>
+
+
><%= impediment.subject %>
+
><%= blocks_ids(impediment.blocks_ids) %>
+
>
+
<%= assignee_name_or_empty(impediment) %>
+
<%= assignee_id_or_empty(impediment) %>
+
+
><%= remaining_hours(impediment) %>
+
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/_backlog.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/_backlog.html.erb
new file mode 100644
index 0000000000..9c3bd26069
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/_backlog.html.erb
@@ -0,0 +1,56 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% folded = current_user.backlogs_preference(:versions_default_fold_state) == "closed" %>
+
+
+
+ <% reset_cycle 'stories' %>
+ <% backlog.stories.each_with_index do |story, index| %>
+ <% higher_item = index == 0 ? nil :
+ backlog.stories[index - 1] %>
+
+ <%= render :partial => "rb_stories/story",
+ :locals => { :story => story,
+ :higher_item => higher_item } %>
+ <% end %>
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/index.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/index.html.erb
new file mode 100644
index 0000000000..a9e825d931
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_master_backlogs/index.html.erb
@@ -0,0 +1,63 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= render :partial => 'shared/backlogs_header' %>
+ <%= stylesheet_link_tag 'backlogs/master_backlog.css' %>
+<% end %>
+
+
+<%= toolbar title: l(:label_backlogs) %>
+
+
+
+
+ <%= render :partial => 'backlog', :collection => @owner_backlogs %>
+
+
+ <%= render :partial => 'backlog', :collection => @sprint_backlogs %>
+
+
+
+
+ <%= render :partial => "rb_stories/helpers" %>
+
<%= date_string_with_milliseconds(@last_update, 0.001) %>
+
+
+<%
+ #include calendar js files in case they are needed for edit
+ include_calendar_headers_tags
+-%>
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_sprints/_sprint.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_sprints/_sprint.html.erb
new file mode 100644
index 0000000000..222408928c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_sprints/_sprint.html.erb
@@ -0,0 +1,55 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+
+
<%= sprint_link_or_empty(sprint) %>
+
<%= id_or_empty(sprint) %>
+
+
+ <% User.current.allowed_to?(:update_sprints, @project) ? editable = "editable" : editable = "" %>
+
>
+ <%= sprint.effective_date %>
+
+
>
+ <%= sprint.start_date %>
+
+
><%= sprint.name %>
+
+
+ <%= render :partial => "shared/model_errors", :object => sprint.errors %>
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_stories/_helpers.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_stories/_helpers.html.erb
new file mode 100644
index 0000000000..cc86e24fb2
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_stories/_helpers.html.erb
@@ -0,0 +1,81 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+<% available_statuses_by_type.each do |type, statuses| %>
+ <% statuses.each do |old_status, allowed_statuses| %>
+
+ <% allowed_statuses.sort_by(&:position).each do |status| %>
+ ">
+ <%= status.name %>
+
+ <% end %>
+
+ <% end %>
+<% end %>
+
+<% all_work_package_status.each do |old_status| %>
+
+ ">
+ <%= default_work_package_status.name %>
+
+ <% if old_status != default_work_package_status %>
+ ">
+ <%= old_status.name %>
+
+ <% end %>
+
+<% end %>
+
+
+ <% all_work_package_status.each do |status| %>
+ ">
+ <%= status.name %>
+
+ <% end %>
+
+
+
+ <% available_story_types.each do |type| %>
+
+ <%= type.name %>
+
+ <% end %>
+
+
+
+ <%= render :partial => 'rb_stories/story', :object => template_story %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_stories/_story.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_stories/_story.html.erb
new file mode 100644
index 0000000000..07e3db4a64
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_stories/_story.html.erb
@@ -0,0 +1,57 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+" id="<%= story_html_id_or_empty(story) %>">
+
+
<%= work_package_link_or_empty(story) %>
+
<%= id_or_empty(story) %>
+
+ ><%= story_points_or_empty(story) %>
+ >
+
<%= status_label_or_default(story) %>
+
<%= status_id_or_default(story) %>
+
+ >
+
<%= type_name_or_empty(story) %>:
+
<%= type_id_or_empty(story) %>
+
+ ><%=h story.subject %>
+ <%= story.fixed_version_id %>
+ <%= !defined?(higher_item) || higher_item.nil? ? '' : higher_item.id %>
+
+ <%= render(:partial => "shared/model_errors", :object => story.errors) if story.errors.size > 0 %>
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_taskboards/show.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_taskboards/show.html.erb
new file mode 100644
index 0000000000..500c8691b5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_taskboards/show.html.erb
@@ -0,0 +1,154 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= render :partial => 'shared/backlogs_header' %>
+ <%= stylesheet_link_tag 'backlogs/taskboard.css' %>
+<% end %>
+
+<%= toolbar title: @sprint.name do %>
+ <% if @sprint.has_burndown? %>
+
+
+ <%= l('backlogs.column_width') %>
+
+
+
+
+ <%= show_burndown_link(@sprint) %>
+
+ <% end %>
+<% end %>
+
+<% breadcrumb_paths(link_to(l(:label_backlogs), backlogs_project_backlogs_path(@project)), link_to(@sprint.name, backlogs_project_backlogs_path(@sprint))) %>
+
+
+
+
+
+
+
+ <%= l(:label_sprint_impediments) %>
+ <% if User.current.allowed_to?(:create_impediments, @project) %>
+ +
+ <% else %>
+
+ <% end %>
+ <% @statuses.each do |status| %>
+
+ <%= render :partial => "rb_impediments/impediment",
+ :collection => impediments_by_position_for_status(@sprint, @project, status) %>
+
+ <% end %>
+
+
+
+
+ <% @sprint.stories(@project).each do |story| %>
+
+
+
+
+
+ <%= story.status.name %>
+
+
+ <%= work_package_link_or_empty(story) %>
+
+
+
<%= story.subject %>
+
+
+
+ <% if User.current.allowed_to?(:create_tasks, @project) %>
+ +
+ <% else %>
+
+ <% end %>
+ <% @statuses.each do |status| %>
+
+ <%= render :partial => "rb_tasks/task",
+ :collection => story.tasks.select { |task| task.status.id == status.id } %>
+
+ <% end %>
+
+ <% end %>
+
+
+
+
+
+
+ <% @project.possible_assignees.each do |user| %>
+
+ <%= user.name %>
+
+ <% end %>
+
+
+ <%= render :partial => "rb_tasks/task", :object => Task.new, :locals => {:project => @project} %>
+
+
+ <%= render :partial => "rb_impediments/impediment", :object => Impediment.new, :locals => {:project => @project} %>
+
+
+
+
<%= date_string_with_milliseconds( (@last_updated.blank? ? Time.now : @last_updated.updated_at) ) %>
+
+
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_tasks/_task.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_tasks/_task.html.erb
new file mode 100644
index 0000000000..3108602df3
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_tasks/_task.html.erb
@@ -0,0 +1,56 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% prevent_edit = User.current.allowed_to?(:update_tasks, defined?(project) ? project : task.project) ? '' : 'prevent_edit'%>
+>
+
+
<%= work_package_link_or_empty(task) %>
+
<%= id_or_empty(task) %>
+
+
><%= task.subject %>
+
>
+
<%= assignee_name_or_empty(task) %>
+
<%= assignee_id_or_empty(task) %>
+
+
<%= remaining_hours(task) %>
+
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/rb_tasks/index.html.erb b/vendored-plugins/openproject-backlogs/app/views/rb_tasks/index.html.erb
new file mode 100644
index 0000000000..5408e6478b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/rb_tasks/index.html.erb
@@ -0,0 +1,43 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+
<%= date_string_with_milliseconds( (@last_updated.blank? ? Time.parse(params[:after]) : @last_updated.updated_on), 0.001 ) %>
+ <%= render :partial => "task", :collection => @tasks, :locals => { :include_meta => @include_meta } %>
+ <%- if @impediments %>
+ <%= render :partial => "impediment", :collection => @impediments, :locals => { :include_meta => @include_meta }%>
+ <%- end %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_backlogs_header.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_backlogs_header.html.erb
new file mode 100644
index 0000000000..9e1e90ae45
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_backlogs_header.html.erb
@@ -0,0 +1,42 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+ <%= stylesheet_link_tag 'backlogs/backlogs.css' %>
+ <%= javascript_include_tag 'backlogs/backlogs.js' %>
+
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_model_errors.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_model_errors.html.erb
new file mode 100644
index 0000000000..153102476e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_model_errors.html.erb
@@ -0,0 +1,41 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+ <% model_errors.full_messages.each do |err| %>
+
<%= err %>
+ <% end %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_server_variables.js.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_server_variables.js.erb
new file mode 100644
index 0000000000..6e1632b110
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_server_variables.js.erb
@@ -0,0 +1,80 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+RB.constants = {
+ project_id: <%= @project.id %>,
+ sprint_id: <%= @sprint ? @sprint.id : "null" %>
+};
+
+RB.i18n = {
+ generating_graph: '<%= l("backlogs.generating_chart") %>',
+ burndown_graph: '<%= l("backlogs.burndown_graph") %>'
+};
+
+RB.urlFor = (function () {
+ var routes = {
+ update_sprint: '<%= backlogs_project_sprint_path(:project_id => @project.identifier, :id => ":id") %>',
+
+ create_story: '<%= backlogs_project_sprint_stories_path(:project_id => @project.identifier, :sprint_id => ":sprint_id") %>',
+ update_story: '<%= backlogs_project_sprint_story_path(:project_id => @project.identifier, :sprint_id => ":sprint_id", :id => ":id") %>',
+
+ create_task: '<%= backlogs_project_sprint_tasks_path(:project_id => @project.identifier, :sprint_id => ":sprint_id") %>',
+ update_task: '<%= backlogs_project_sprint_task_path(:project_id => @project.identifier, :sprint_id => ":sprint_id", :id => ":id") %>',
+
+ create_impediment: '<%= backlogs_project_sprint_impediments_path(:project_id => @project.identifier, :sprint_id => ":sprint_id") %>',
+ update_impediment: '<%= backlogs_project_sprint_impediment_path(:project_id => @project.identifier, :sprint_id => ":sprint_id", :id => ":id") %>',
+
+ show_burndown_chart: '<%= backlogs_project_sprint_burndown_chart_path(:project_id => @project.identifier, :sprint_id => ":sprint_id") %>'
+ };
+
+ return function (routeName, options) {
+ route = routes[routeName];
+
+ if (options){
+ if (options.id) {
+ route = route.replace(":id", options.id);
+ }
+ if (options.project_id){
+ route = route.replace(":project_id", options.project_id);
+ }
+ if(options.sprint_id) {
+ route = route.replace(":sprint_id", options.sprint_id)
+ }
+ }
+
+ return route;
+ }
+}());
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_settings.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_settings.html.erb
new file mode 100644
index 0000000000..8477f102f5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_settings.html.erb
@@ -0,0 +1,90 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% content_for :header_tags do %>
+
+<% end %>
+
+
+ <%= styled_label_tag("settings[story_types]", l(:backlogs_story_type)) %>
+ <%= styled_select_tag("settings[story_types]",
+ options_from_collection_for_select(Type.all, :id, :name, Story.types), :multiple => true) %>
+
+
+ <%= styled_label_tag("settings[task_type]", l(:backlogs_task_type)) %>
+ <%= styled_select_tag("settings[task_type]",
+ options_from_collection_for_select(Type.all, :id, :name, Task.type)) %>
+
+
+ <%= styled_label_tag("settings[points_burn_direction]", l(:backlogs_points_burn_direction)) %>
+ <%= styled_select_tag("settings[points_burn_direction]",
+ options_for_select([[l(:label_points_burn_up), 'up'], [l(:label_points_burn_down), 'down']],
+ Setting.plugin_openproject_backlogs["points_burn_direction"])) %>
+
+
+ <%= styled_label_tag("settings[wiki_template]", l(:backlogs_wiki_template)) %>
+ <%= styled_text_field_tag("settings[wiki_template]",
+ Setting.plugin_openproject_backlogs["wiki_template"]) %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_validation_errors.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_validation_errors.html.erb
new file mode 100644
index 0000000000..b74bda036a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_validation_errors.html.erb
@@ -0,0 +1,43 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= validation_errors.length > 1 ? l(:error_intro_plural) : l(:error_intro_singular) %>
+
+ <%- validation_errors.each_full do |msg| %>
+ <%= msg %>
+ <%- end %>
+
+<%= l(:error_outro ) %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/_view_my_settings.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/_view_my_settings.html.erb
new file mode 100644
index 0000000000..901d3c3c52
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/_view_my_settings.html.erb
@@ -0,0 +1,45 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+
+ <%= styled_label_tag :backlogs_task_color, l(:'backlogs.task_color') %>
+ <%= styled_text_field_tag :'backlogs[task_color]', color, container_class: '-middle' %>
+
+
+ <%= styled_label_tag :backlogs_versions_default_fold_state, I18n.t('backlogs.label_versions_default_fold_state') %>
+ <%= styled_check_box_tag :"backlogs[versions_default_fold_state]", "closed", versions_default_fold_state == "closed" %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/shared/not_configured.html.erb b/vendored-plugins/openproject-backlogs/app/views/shared/not_configured.html.erb
new file mode 100644
index 0000000000..a91d71a9dc
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/shared/not_configured.html.erb
@@ -0,0 +1,44 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+
+ <%= l(:label_backlogs_unconfigured, {
+ :administration => l(:label_administration),
+ :plugins => l(:label_plugins),
+ :configure => l(:button_configure)
+ })
+ %>
+
diff --git a/vendored-plugins/openproject-backlogs/app/views/version_settings/_form.html.erb b/vendored-plugins/openproject-backlogs/app/views/version_settings/_form.html.erb
new file mode 100644
index 0000000000..0bc24a855e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/version_settings/_form.html.erb
@@ -0,0 +1,41 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= error_messages_for 'version' %>
+
+<% if @project.enabled_modules.map(&:name).include?("backlogs") %>
+ <%= version_settings_fields @version, @project %>
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/version_settings/edit.html.erb b/vendored-plugins/openproject-backlogs/app/views/version_settings/edit.html.erb
new file mode 100644
index 0000000000..d980128602
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/version_settings/edit.html.erb
@@ -0,0 +1,41 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+<%= toolbar title: l(:label_version) %>
+
+<% labelled_tabular_form_for :version, @version, :url => project_version_path(@project, @version), :html => {:method => :put} do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
+<% end %>
diff --git a/vendored-plugins/openproject-backlogs/app/views/versions/_form.html.erb b/vendored-plugins/openproject-backlogs/app/views/versions/_form.html.erb
new file mode 100644
index 0000000000..54b34779d7
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/app/views/versions/_form.html.erb
@@ -0,0 +1,75 @@
+<%#-- copyright
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= error_messages_for 'version' %>
+<%= back_url_hidden_field_tag %>
+
+<% if @version.project == @project %>
+
+ <%= f.text_field :name, required: true %>
+
+
+ <%= f.text_field :description %>
+
+
+ <%= f.select :status, Version::VERSION_STATUSES.map {|s| [l("version_status_#{s}"), s]} %>
+
+
+ <%= f.text_field :wiki_page_title, label: :label_wiki_page, disabled: @project.wiki.nil? %>
+
+
+ <%= f.text_field :start_date %><%= calendar_for('version_start_date') %>
+
+
+ <%= f.text_field :effective_date %><%= calendar_for('version_effective_date') %>
+
+
+ <%= f.select :sharing, @version.allowed_sharings.map {|v| [format_version_sharing(v), v]} %>
+
+<% end %>
+
+<% if @project.enabled_modules.map(&:name).include?("backlogs") %>
+
+ <%= version_settings_fields(@version, @project).html_safe %>
+
+<% end %>
+
+<% if @version.project == @project %>
+ <% @version.custom_field_values.each do |value| %>
+ <%= custom_field_tag_with_label :version, value %>
+ <% end %>
+<% end %>
+
diff --git a/vendored-plugins/openproject-backlogs/config/cucumber.yml b/vendored-plugins/openproject-backlogs/config/cucumber.yml
new file mode 100644
index 0000000000..2956d7df2e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/cucumber.yml
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+<%
+rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
+rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
+std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
+%>
+default: <%= std_opts %> features -r <%= "#{ENV['RAILS_ROOT']}/features" %> -r features --guess
+wip: --tags @wip:3 --wip features -r <%= "#{ENV['RAILS_ROOT']}/features" %> -r features --guess
+rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
diff --git a/vendored-plugins/openproject-backlogs/config/labels-malformed.yml b/vendored-plugins/openproject-backlogs/config/labels-malformed.yml
new file mode 100644
index 0000000000..b524177beb
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/labels-malformed.yml
@@ -0,0 +1,72 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+---
+Worldlabel WL-175:
+ across: 1
+ inner_margin: 0.0625in
+ vertical_pitch: 0pt
+ down: 1
+ papersize: Letter
+ top_margin: 0pt
+ horizontal_pitch: 0pt
+ height: 11.in
+ source: glabel
+ width: 8.5in
+ left_margin: 0pt
+PEARL VM-6252:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 0mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Avery 8165:
+ across: 1
+ inner_margin: 0.0625in
+ vertical_pitch: 0pt
+ down: 1
+ papersize: Letter
+ top_margin: 0pt
+ horizontal_pitch: 0pt
+ height: 11.in
+ source: glabel
+ width: 8.5in
+ left_margin: 0pt
diff --git a/vendored-plugins/openproject-backlogs/config/labels.yml b/vendored-plugins/openproject-backlogs/config/labels.yml
new file mode 100644
index 0000000000..e14abef451
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/labels.yml
@@ -0,0 +1,228 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+---
+PEARL PE-8007:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Avery 8435B:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 122mm
+ down: 1
+ papersize: A4
+ top_margin: 21mm
+ horizontal_pitch: 155mm
+ height: 122mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
+PEARL VM-5513-1:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 130mm
+ down: 1
+ papersize: A4
+ top_margin: 24mm
+ horizontal_pitch: 130mm
+ height: 120mm
+ source: glabel
+ width: 120mm
+ left_margin: 61mm
+PEARL VM-5513-2:
+ across: 1
+ inner_margin: 1mm
+ vertical_pitch: 127.5mm
+ down: 1
+ papersize: A4
+ top_margin: 154.5mm
+ horizontal_pitch: 160mm
+ height: 117.5mm
+ source: glabel
+ width: 150mm
+ left_margin: 30.5mm
+PEARL PE-8350:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+PEARL PE-8351:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Zweckform J8435A:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 125mm
+ down: 1
+ papersize: A4
+ top_margin: 22mm
+ horizontal_pitch: 155mm
+ height: 125mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
+Zweckform J8435B:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 122mm
+ down: 1
+ papersize: A4
+ top_margin: 21mm
+ horizontal_pitch: 155mm
+ height: 122mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
+PEARL VM-5220:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+PEARL VM-5081:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+PEARL VM-6206:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+PEARL VM-6630:
+ across: 1
+ inner_margin: 1mm
+ vertical_pitch: 286mm
+ down: 1
+ papersize: A4
+ top_margin: 10.5mm
+ horizontal_pitch: 192mm
+ height: 276mm
+ source: glabel
+ width: 182mm
+ left_margin: 14mm
+PEARL VM-6096:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 148mm
+ down: 1
+ papersize: A6
+ top_margin: 0mm
+ horizontal_pitch: 105mm
+ height: 148mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Neato 69143:
+ across: 1
+ inner_margin: 0.125in
+ vertical_pitch: 9.5in
+ down: 1
+ papersize: Letter
+ top_margin: 0.75in
+ horizontal_pitch: 4.75in
+ height: 9.5in
+ source: glabel
+ width: 4.75in
+ left_margin: 1.875in
+PEARL VM-6097:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 210mm
+ down: 1
+ papersize: A5
+ top_margin: 0mm
+ horizontal_pitch: 148mm
+ height: 210mm
+ source: glabel
+ width: 148mm
+ left_margin: 0mm
+Avery 8435A:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 125mm
+ down: 1
+ papersize: A4
+ top_margin: 22mm
+ horizontal_pitch: 155mm
+ height: 125mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
diff --git a/vendored-plugins/openproject-backlogs/config/labels.yml.default b/vendored-plugins/openproject-backlogs/config/labels.yml.default
new file mode 100644
index 0000000000..3bf2087e33
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/labels.yml.default
@@ -0,0 +1,2521 @@
+---
+Avery 8373:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2.5in
+ down: 4
+ papersize: Letter
+ top_margin: 0.75in
+ horizontal_pitch: 4in
+ height: 2in
+ source: glabel
+ width: 3.5in
+ left_margin: 0.5in
+Avery 8435B:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 122mm
+ down: 1
+ papersize: A4
+ top_margin: 21mm
+ horizontal_pitch: 155mm
+ height: 122mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
+Sattleford VM-6253, VM-6753:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 55.5mm
+ down: 5
+ papersize: A4
+ top_margin: 6mm
+ horizontal_pitch: 90mm
+ height: 54mm
+ source: glabel
+ width: 85mm
+ left_margin: 16.5mm
+Agipa 119488:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 142pt
+ down: 5
+ papersize: A4
+ top_margin: 67pt
+ horizontal_pitch: 255pt
+ height: 142pt
+ source: glabel
+ width: 255pt
+ left_margin: 43pt
+Ednet BC:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 145pt
+ down: 5
+ papersize: A4
+ top_margin: 59pt
+ horizontal_pitch: 253pt
+ height: 145pt
+ source: glabel
+ width: 253pt
+ left_margin: 40pt
+DECAdry OLW-4734:
+ across: 2
+ inner_margin: 3.175mm
+ vertical_pitch: 67.7mm
+ down: 4
+ top_margin: 13.1mm
+ papersize: A4
+ height: 67.7mm
+ horizontal_pitch: 101.5mm
+ source: glabel
+ left_margin: 4.65mm
+ width: 99.1mm
+Dymo 30258:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 198pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 153pt
+ height: 198pt
+ source: glabel
+ width: 153pt
+ left_margin: 0pt
+Connect KF10647:
+ across: 3
+ inner_margin: 9pt
+ vertical_pitch: 104.882pt
+ down: 8
+ papersize: A4
+ top_margin: 11.3386pt
+ horizontal_pitch: 198.425pt
+ height: 104.882pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 7.86772e-17pt
+Herma 4828:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 131.811pt
+ down: 6
+ papersize: A4
+ top_margin: 24.9449pt
+ horizontal_pitch: 295.086pt
+ height: 131.811pt
+ source: glabel
+ width: 223.086pt
+ left_margin: 38.5512pt
+Russian A4 5x9:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 90mm
+ down: 3
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 50mm
+ height: 90mm
+ source: glabel
+ width: 50mm
+ left_margin: 5mm
+Zweckform 3477:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 41mm
+ down: 7
+ papersize: A4
+ top_margin: 4.93mm
+ horizontal_pitch: 105mm
+ height: 41mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Sattleford VM-6102, VM-6502:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 65mm
+ down: 4
+ papersize: A4
+ top_margin: 23.5mm
+ horizontal_pitch: 55mm
+ height: 55mm
+ source: glabel
+ width: 45mm
+ left_margin: 27mm
+Worldlabel WL-250:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1in
+ horizontal_pitch: 4.125in
+ height: 1.5in
+ source: glabel
+ width: 4in
+ left_margin: 0.1875in
+Avery 8658:
+ across: 7
+ inner_margin: 2mm
+ vertical_pitch: 10.14mm
+ down: 27
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 28.5mm
+ height: 10mm
+ source: glabel
+ width: 25.4mm
+ left_margin: 5mm
+PEARL VM-6232, VM-6732:
+ across: 2
+ inner_margin: 2mm
+ vertical_pitch: 42.1mm
+ down: 6
+ papersize: A4
+ top_margin: 21mm
+ horizontal_pitch: 96.5mm
+ height: 42.1mm
+ source: glabel
+ width: 96.5mm
+ left_margin: 8.5mm
+PEARL VM-8005, VM-8015:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 41mm
+ down: 7
+ papersize: A4
+ top_margin: 5mm
+ horizontal_pitch: 105mm
+ height: 41mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Ryman P14:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 38.1mm
+ down: 7
+ papersize: A4
+ top_margin: 13.8mm
+ horizontal_pitch: 102mm
+ height: 38.1mm
+ source: glabel
+ width: 99.1mm
+ left_margin: 4.2mm
+PEARL PE-8008, PE-8018:
+ across: 4
+ inner_margin: 3.2mm
+ vertical_pitch: 25.4mm
+ down: 10
+ papersize: A4
+ top_margin: 21.5mm
+ horizontal_pitch: 48.4mm
+ height: 25.4mm
+ source: glabel
+ width: 48.4mm
+ left_margin: 8mm
+Herma 4293:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 107.716pt
+ down: 7
+ papersize: A4
+ top_margin: 42.5197pt
+ horizontal_pitch: 544.252pt
+ height: 107.716pt
+ source: glabel
+ width: 544.252pt
+ left_margin: 25.5118pt
+DECAdry OLW-7131:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 108.0pt
+ down: 7
+ top_margin: 43.9pt
+ papersize: A4
+ height: 108.0pt
+ horizontal_pitch: 187.2pt
+ source: glabel
+ left_margin: 21.2pt
+ width: 181.4pt
+Sattleford VM-6066, VM-6566:
+ across: 2
+ inner_margin: 3.175mm
+ vertical_pitch: 54mm
+ down: 5
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 95mm
+ height: 54mm
+ source: glabel
+ width: 85mm
+ left_margin: 14mm
+Sattleford VM-5061, VM-5561:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 55.8mm
+ down: 5
+ papersize: A4
+ top_margin: 10mm
+ horizontal_pitch: 91mm
+ height: 53.8mm
+ source: glabel
+ width: 86mm
+ left_margin: 16.5mm
+PEARL VM-5220, VM-5720:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Avery 7169:
+ across: 2
+ inner_margin: 6pt
+ vertical_pitch: 394.0pt
+ down: 2
+ papersize: A4
+ top_margin: 20.0pt
+ horizontal_pitch: 287.7pt
+ height: 394.0pt
+ source: glabel
+ width: 280.9pt
+ left_margin: 14.2pt
+Impact LC33:
+ across: 3
+ inner_margin: 3mm
+ vertical_pitch: 25mm
+ down: 11
+ papersize: A4
+ top_margin: 10mm
+ horizontal_pitch: 70mm
+ height: 25mm
+ source: glabel
+ width: 70mm
+ left_margin: 0mm
+PEARL PE-8004, PE-8014:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 36mm
+ down: 8
+ papersize: A4
+ top_margin: 4.8mm
+ horizontal_pitch: 70mm
+ height: 36mm
+ source: glabel
+ width: 70mm
+ left_margin: 0mm
+Avery LSK-3:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.38in
+ height: 0.5in
+ source: glabel
+ width: 3.13in
+ left_margin: 0.5in
+Avery 5663:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.25in
+ height: 2in
+ source: glabel
+ width: 4.25in
+ left_margin: 0in
+Zweckform 32250A:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 125mm
+ down: 1
+ top_margin: 22mm
+ papersize: A4
+ height: 125mm
+ horizontal_pitch: 155mm
+ source: glabel
+ left_margin: 27.5mm
+ width: 155mm
+Herma 4464:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 105pt
+ down: 8
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 199pt
+ height: 105pt
+ source: glabel
+ width: 199pt
+ left_margin: 0pt
+Worldlabel WL-5100:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 5in
+ down: 2
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4in
+ height: 5in
+ source: glabel
+ width: 3.5in
+ left_margin: 0.5in
+Worldlabel WL-225:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 3in
+ down: 3
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.75in
+ height: 2.75in
+ source: glabel
+ width: 2.75in
+ left_margin: 0.125in
+Avery 6871:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 2.69in
+ height: 1.25in
+ source: glabel
+ width: 2.375in
+ left_margin: 0.3725in
+Avery LSK-8.5:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.13in
+ height: 0.5in
+ source: glabel
+ width: 1.13in
+ left_margin: 0.5in
+Worldlabel WL-875:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 1in
+ down: 10
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.75in
+ height: 1in
+ source: glabel
+ width: 2.625in
+ left_margin: 0.1875in
+Zweckform 4760:
+ across: 1
+ inner_margin: 9pt
+ vertical_pitch: 107.717pt
+ down: 7
+ papersize: A4
+ top_margin: 43.7386pt
+ horizontal_pitch: 544.252pt
+ height: 107.717pt
+ source: glabel
+ width: 544.252pt
+ left_margin: 25.5118pt
+Avery LSK-5:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.3in
+ horizontal_pitch: 2.05in
+ height: 0.5in
+ source: glabel
+ width: 1.75in
+ left_margin: 0.5in
+Zweckform 4732:
+ across: 5
+ inner_margin: 5.66929pt
+ vertical_pitch: 48.189pt
+ down: 16
+ papersize: A4
+ top_margin: 36.8504pt
+ horizontal_pitch: 107.717pt
+ height: 48.189pt
+ source: glabel
+ width: 102.047pt
+ left_margin: 31.1811pt
+DataBecker 0491:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 54mm
+ down: 5
+ papersize: A4
+ top_margin: 15mm
+ horizontal_pitch: 94.55mm
+ height: 50mm
+ source: glabel
+ width: 90mm
+ left_margin: 12.5mm
+Herma 4606:
+ across: 5
+ inner_margin: 5pt
+ vertical_pitch: 60.0945pt
+ down: 13
+ papersize: A4
+ top_margin: 29.7638pt
+ horizontal_pitch: 107.716pt
+ height: 60.0945pt
+ source: glabel
+ width: 107.716pt
+ left_margin: 26.9291pt
+Zweckform 32250B:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 122mm
+ down: 1
+ top_margin: 21mm
+ papersize: A4
+ height: 122mm
+ horizontal_pitch: 155mm
+ source: glabel
+ left_margin: 27.5mm
+ width: 155mm
+APLI 02793:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ papersize: A4
+ top_margin: 62.3622pt
+ horizontal_pitch: 255.118pt
+ height: 144pt
+ source: glabel
+ width: 255.118pt
+ left_margin: 42.5197pt
+Zweckform 4761:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 172.9pt
+ down: 4
+ papersize: A4
+ top_margin: 75pt
+ horizontal_pitch: 544.25pt
+ height: 172.9pt
+ source: glabel
+ width: 544.25pt
+ left_margin: 25pt
+Avery 6873:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2.25in
+ down: 4
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 4.0in
+ height: 2.0in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+DataBecker BC?:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 142pt
+ down: 5
+ papersize: A4
+ top_margin: 69.890pt
+ horizontal_pitch: 255pt
+ height: 142pt
+ source: glabel
+ width: 255pt
+ left_margin: 36pt
+Poundland 30:
+ across: 3
+ inner_margin: 7.08661pt
+ vertical_pitch: 72pt
+ down: 10
+ papersize: A4
+ top_margin: 62.3622pt
+ horizontal_pitch: 201.26pt
+ height: 72pt
+ source: glabel
+ width: 184.252pt
+ left_margin: 4.25197pt
+Dymo 30376:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 144pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 41pt
+ height: 144pt
+ source: glabel
+ width: 41pt
+ left_margin: 0pt
+DECAdry OCC 3342:
+ across: 2
+ inner_margin: 0pt
+ vertical_pitch: 153.071pt
+ down: 5
+ papersize: A4
+ top_margin: 38.2677pt
+ horizontal_pitch: 269.291pt
+ height: 153.071pt
+ source: glabel
+ width: 240.945pt
+ left_margin: 42.5197pt
+Avery 6874:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 3.375in
+ down: 3
+ papersize: Letter
+ top_margin: 0.625in
+ horizontal_pitch: 4.0in
+ height: 3.0in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+Herma 4607:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 47.9055pt
+ down: 16
+ papersize: A4
+ top_margin: 39.6853pt
+ horizontal_pitch: 136.913pt
+ height: 47.9055pt
+ source: glabel
+ width: 136.913pt
+ left_margin: 24.0945pt
+Worldlabel WL-75:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1in
+ down: 10
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 1in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Avery 5159:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 7
+ papersize: Letter
+ top_margin: 0.25in
+ horizontal_pitch: 4.1875in
+ height: 1.5in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Avery 5931-Spine:
+ across: 2
+ inner_margin: 1mm
+ vertical_pitch: 4.84375in
+ down: 2
+ papersize: Letter
+ top_margin: 0.734375in
+ horizontal_pitch: 0.46875in
+ height: 4.6875in
+ source: glabel
+ width: 0.21875in
+ left_margin: 0.5in
+Zweckform 3651:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 83.6220pt
+ down: 10
+ papersize: A4
+ top_margin: 2.834pt
+ horizontal_pitch: 147.4016pt
+ height: 83.6220pt
+ source: glabel
+ width: 147.4016pt
+ left_margin: 2.834pt
+Avery LSK-8:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.13in
+ height: 0.5in
+ source: glabel
+ width: 1.13in
+ left_margin: 0.5in
+Maco LL5805:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 1in
+ down: 10
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.75in
+ height: 1in
+ source: glabel
+ width: 2.625in
+ left_margin: 0.1875in
+Worldlabel WL-171:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2.25in
+ down: 4
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 4.0in
+ height: 2.0in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+Herma 4608:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 72pt
+ down: 11
+ papersize: A4
+ top_margin: 24.0945pt
+ horizontal_pitch: 136.913pt
+ height: 72pt
+ source: glabel
+ width: 136.913pt
+ left_margin: 24.0945pt
+Herma 5051:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 72pt
+ down: 11
+ top_margin: 24.0945pt
+ papersize: A4
+ height: 72pt
+ horizontal_pitch: 136.913pt
+ source: glabel
+ left_margin: 24.0945pt
+ width: 136.913pt
+Worldlabel WL-172:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 3.375in
+ down: 3
+ papersize: Letter
+ top_margin: 0.625in
+ horizontal_pitch: 4.0in
+ height: 3.0in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+Sattleford VM-5019, VM-5519:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 54.2mm
+ down: 5
+ papersize: A4
+ top_margin: 13.5mm
+ horizontal_pitch: 95mm
+ height: 54.2mm
+ source: glabel
+ width: 85mm
+ left_margin: 13.5mm
+Staples EU BC Supreme:
+ across: 1
+ inner_margin: 9.07087pt
+ vertical_pitch: 155.906pt
+ down: 5
+ papersize: A4
+ top_margin: 28.3465pt
+ horizontal_pitch: 240.945pt
+ height: 155.906pt
+ source: glabel
+ width: 240.945pt
+ left_margin: 311.811pt
+Worldlabel WL-625:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.25in
+ height: 2in
+ source: glabel
+ width: 4.25in
+ left_margin: 0in
+Avery 3274.1:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 3in
+ down: 3
+ papersize: Letter
+ top_margin: 1.25in
+ horizontal_pitch: 2.6875in
+ height: 2.5in
+ source: glabel
+ width: 2.5in
+ left_margin: 0.3125in
+Avery 6141:
+ across: 1
+ inner_margin: 4.5pt
+ vertical_pitch: 49.5pt
+ down: 7
+ papersize: Other
+ top_margin: 4.5pt
+ horizontal_pitch: 198pt
+ height: 45pt
+ source: glabel
+ width: 198pt
+ left_margin: 4.5pt
+Worldlabel WL-200:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 0.666666667in
+ down: 15
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4in
+ height: 0.666666667in
+ source: glabel
+ width: 3.4375in
+ left_margin: 0.53125in
+Herma 5053:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 95.811pt
+ down: 8
+ top_margin: 36.8504pt
+ papersize: A4
+ height: 95.811pt
+ horizontal_pitch: 187.086pt
+ source: glabel
+ left_margin: 16.1575pt
+ width: 187.086pt
+Herma 4270:
+ across: 5
+ inner_margin: 5pt
+ vertical_pitch: 60.0945pt
+ down: 13
+ top_margin: 29.7638pt
+ papersize: A4
+ height: 60.0945pt
+ horizontal_pitch: 107.716pt
+ source: glabel
+ left_margin: 26.9291pt
+ width: 107.716pt
+Avery 5160:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 1in
+ down: 10
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.75in
+ height: 1in
+ source: glabel
+ width: 2.625in
+ left_margin: 0.1875in
+PEARL PE-8351, PE-894:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Avery 5388:
+ across: 1
+ inner_margin: 0.125in
+ vertical_pitch: 3in
+ down: 3
+ papersize: Letter
+ top_margin: 1in
+ horizontal_pitch: 5in
+ height: 3in
+ source: glabel
+ width: 5in
+ left_margin: 1.75in
+OfficeMax OM99060:
+ across: 3
+ inner_margin: 12.024pt
+ vertical_pitch: 72pt
+ down: 10
+ papersize: Letter
+ top_margin: 36pt
+ horizontal_pitch: 203.76pt
+ height: 72pt
+ source: glabel
+ width: 203.76pt
+ left_margin: 0pt
+Herma 4271:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 47.9055pt
+ down: 16
+ top_margin: 39.6853pt
+ papersize: A4
+ height: 47.9055pt
+ horizontal_pitch: 136.913pt
+ source: glabel
+ left_margin: 24.0945pt
+ width: 136.913pt
+PEARL VM-6234, VM-6734:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 57mm
+ down: 5
+ papersize: A4
+ top_margin: 6mm
+ horizontal_pitch: 102.1mm
+ height: 57mm
+ source: glabel
+ width: 99.1mm
+ left_margin: 5mm
+Avery 6879:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 4in
+ height: 1.25in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+Avery 5389:
+ across: 1
+ inner_margin: 0.125in
+ vertical_pitch: 4.5in
+ down: 2
+ papersize: Letter
+ top_margin: 1.25in
+ horizontal_pitch: 6in
+ height: 4in
+ source: glabel
+ width: 6in
+ left_margin: 1.25in
+Herma 4272:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 72pt
+ down: 11
+ top_margin: 24.0945pt
+ papersize: A4
+ height: 72pt
+ horizontal_pitch: 136.913pt
+ source: glabel
+ left_margin: 24.0945pt
+ width: 136.913pt
+Avery 5161:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1in
+ down: 10
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 1in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Avery 18036:
+ across: 3
+ inner_margin: 10pt
+ vertical_pitch: 91pt
+ down: 9
+ papersize: A4
+ top_margin: 13pt
+ horizontal_pitch: 198pt
+ height: 91pt
+ source: glabel
+ width: 198pt
+ left_margin: 0pt
+Sigel DP 830:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 156pt
+ down: 5
+ papersize: A4
+ top_margin: 31pt
+ horizontal_pitch: 269pt
+ height: 155.9pt
+ source: glabel
+ width: 240.9pt
+ left_margin: 40pt
+Avery 5162:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.333333333in
+ down: 7
+ papersize: Letter
+ top_margin: 0.833333333in
+ horizontal_pitch: 4.1875in
+ height: 1.333333333in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Herma 5057:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 119.905pt
+ down: 7
+ top_margin: 0pt
+ papersize: A4
+ height: 119.905pt
+ horizontal_pitch: 297.638pt
+ source: glabel
+ left_margin: 0pt
+ width: 297.638pt
+DataBecker 0526:
+ across: 3
+ inner_margin: 9pt
+ vertical_pitch: 102.047pt
+ down: 8
+ papersize: A4
+ top_margin: 15.5906pt
+ horizontal_pitch: 198.425pt
+ height: 102.047pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 3.93386e-16pt
+Your Design VM-5112, VM-5612:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 68.75mm
+ down: 4
+ papersize: A4
+ top_margin: 21mm
+ horizontal_pitch: 100mm
+ height: 54.25mm
+ source: glabel
+ width: 85mm
+ left_margin: 12mm
+Avery 5163:
+ across: 2
+ inner_margin: 0.125in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 2in
+ source: glabel
+ width: 4in
+ left_margin: 0.1625in
+Avery L4732:
+ across: 5
+ inner_margin: 1mm
+ vertical_pitch: 16.9mm
+ down: 16
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 38.1mm
+ height: 16.9mm
+ source: glabel
+ width: 35.6mm
+ left_margin: 11mm
+Avery 32015:
+ across: 2
+ inner_margin: 5mm
+ vertical_pitch: 60mm
+ down: 4
+ papersize: A4
+ top_margin: 31.5mm
+ horizontal_pitch: 91mm
+ height: 54mm
+ source: glabel
+ width: 85mm
+ left_margin: 17mm
+Avery LSK-3.5:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.38in
+ height: 0.5in
+ source: glabel
+ width: 3.13in
+ left_margin: 0.5in
+Avery 5164:
+ across: 2
+ inner_margin: 0.125in
+ vertical_pitch: 3.333333333in
+ down: 3
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 3.333333333in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Herma 8803:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ papersize: A4
+ top_margin: 60.9448pt
+ horizontal_pitch: 270.425pt
+ height: 144pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 60.9448pt
+Biltema 23-756:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 204.038pt
+ down: 4
+ top_margin: 10pt
+ papersize: A4
+ height: 204.038pt
+ horizontal_pitch: 187.08pt
+ source: glabel
+ left_margin: 20.84pt
+ width: 180pt
+Worldlabel WL-25:
+ across: 4
+ inner_margin: 0.0625in
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.0625in
+ height: 0.5in
+ source: glabel
+ width: 1.75in
+ left_margin: 0.28125in
+Zweckform 3348:
+ across: 3
+ inner_margin: 9pt
+ vertical_pitch: 107.717pt
+ down: 7
+ papersize: A4
+ top_margin: 56.6929pt
+ horizontal_pitch: 198.425pt
+ height: 107.717pt
+ source: glabel
+ width: 189.921pt
+ left_margin: 5.66929pt
+Herma 4614:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 95.811pt
+ down: 8
+ papersize: A4
+ top_margin: 36.8504pt
+ horizontal_pitch: 187.086pt
+ height: 95.811pt
+ source: glabel
+ width: 187.086pt
+ left_margin: 16.1575pt
+Zweckform 3659:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 120.5pt
+ down: 6
+ papersize: A4
+ top_margin: 60pt
+ horizontal_pitch: 274pt
+ height: 120.5pt
+ source: glabel
+ width: 274pt
+ left_margin: 22.68pt
+Dymo 99010:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 252pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 81pt
+ height: 252pt
+ source: glabel
+ width: 81pt
+ left_margin: 0pt
+Herma 4670:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 95.811pt
+ down: 8
+ top_margin: 36.8504pt
+ papersize: A4
+ height: 95.811pt
+ horizontal_pitch: 187.086pt
+ source: glabel
+ left_margin: 16.1575pt
+ width: 187.086pt
+PEARL VM-6398:
+ across: 9
+ inner_margin: 1.5mm
+ vertical_pitch: 28mm
+ down: 10
+ papersize: A4
+ top_margin: 10mm
+ horizontal_pitch: 22.6mm
+ height: 25mm
+ source: glabel
+ width: 19.6mm
+ left_margin: 4.5mm
+APLI 1999:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 194pt
+ down: 4
+ papersize: A4
+ top_margin: 21.890pt
+ horizontal_pitch: 289pt
+ height: 193pt
+ source: glabel
+ width: 281pt
+ left_margin: 13pt
+Avery 8414:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ papersize: A4
+ top_margin: 57.890pt
+ horizontal_pitch: 264pt
+ height: 144pt
+ source: glabel
+ width: 246.6pt
+ left_margin: 42pt
+Zweckform 3688:
+ across: 1
+ inner_margin: 9.07087pt
+ vertical_pitch: 172.913pt
+ down: 4
+ papersize: A4
+ top_margin: 76.5354pt
+ horizontal_pitch: 544.252pt
+ height: 172.913pt
+ source: glabel
+ width: 544.252pt
+ left_margin: 28.3465pt
+Dymo 30327:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 248pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 41pt
+ height: 248pt
+ source: glabel
+ width: 41pt
+ left_margin: 0pt
+Avery 6570:
+ across: 4
+ inner_margin: 9pt
+ vertical_pitch: 90pt
+ down: 8
+ papersize: Letter
+ top_margin: 36pt
+ horizontal_pitch: 139.536pt
+ height: 90pt
+ source: glabel
+ width: 126pt
+ left_margin: 36pt
+Kingdom L:
+ across: 2
+ inner_margin: 9pt
+ vertical_pitch: 126.144pt
+ down: 6
+ papersize: Letter
+ top_margin: 27pt
+ horizontal_pitch: 261pt
+ height: 117pt
+ source: glabel
+ width: 252pt
+ left_margin: 33.12pt
+Worldlabel WL-150:
+ across: 2
+ inner_margin: 0.125in
+ vertical_pitch: 3.333333333in
+ down: 3
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 3.333333333in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Dymo 99012:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 252.283pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 102.047pt
+ height: 252.283pt
+ source: glabel
+ width: 102.047pt
+ left_margin: 0pt
+Avery 5167:
+ across: 4
+ inner_margin: 0.0625in
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.0625in
+ height: 0.5in
+ source: glabel
+ width: 1.75in
+ left_margin: 0.28125in
+PEARL VM-6206, VM-6706:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+PEARL VM-6096, VM-6596:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 148mm
+ down: 1
+ papersize: A6
+ top_margin: 0mm
+ horizontal_pitch: 105mm
+ height: 148mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Avery LSK-5.5:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 0.5in
+ down: 20
+ papersize: Letter
+ top_margin: 0.3in
+ horizontal_pitch: 2.05in
+ height: 0.5in
+ source: glabel
+ width: 1.75in
+ left_margin: 0.5in
+Zweckform 6021:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 47.9055pt
+ down: 16
+ papersize: A4
+ top_margin: 39.6853pt
+ horizontal_pitch: 136.913pt
+ height: 47.9055pt
+ source: glabel
+ width: 130.392pt
+ left_margin: 24.0945pt
+Sattleford VM-6067, VM-6567:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 54mm
+ down: 5
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 95mm
+ height: 54mm
+ source: glabel
+ width: 85mm
+ left_margin: 15mm
+Sattleford VM-6700:
+ across: 1
+ inner_margin: 1mm
+ vertical_pitch: 55mm
+ down: 5
+ papersize: A4
+ top_margin: 14mm
+ horizontal_pitch: 180mm
+ height: 50mm
+ source: glabel
+ width: 180mm
+ left_margin: 15mm
+Zweckform 3490:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 102.047pt
+ down: 8
+ papersize: A4
+ top_margin: 12.557pt
+ horizontal_pitch: 198.425pt
+ height: 102.047pt
+ source: glabel
+ width: 198.425pt
+ left_margin: ""
+Russian A3 5x9:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 50mm
+ down: 8
+ papersize: A3
+ top_margin: 10mm
+ horizontal_pitch: 90mm
+ height: 50mm
+ source: glabel
+ width: 90mm
+ left_margin: 14mm
+Avery 5196:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 3in
+ down: 3
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.75in
+ height: 2.75in
+ source: glabel
+ width: 2.75in
+ left_margin: 0.125in
+Avery 5168:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 5in
+ down: 2
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4in
+ height: 5in
+ source: glabel
+ width: 3.5in
+ left_margin: 0.5in
+Avery 5026:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.13in
+ down: 9
+ papersize: Letter
+ top_margin: 0.51125in
+ horizontal_pitch: 4.0674in
+ height: 0.9375in
+ source: glabel
+ width: 3.4375in
+ left_margin: 0.49755in
+Geha Z53:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 38.1mm
+ down: 7
+ papersize: A4
+ top_margin: 15.9mm
+ horizontal_pitch: 101.6mm
+ height: 38.1mm
+ source: glabel
+ width: 99.1mm
+ left_margin: 3mm
+Herma 4674:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 119.905pt
+ down: 7
+ top_margin: 0pt
+ papersize: A4
+ height: 119.905pt
+ horizontal_pitch: 297.638pt
+ source: glabel
+ left_margin: 0pt
+ width: 297.638pt
+Zweckform J8435A:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 125mm
+ down: 1
+ top_margin: 22mm
+ papersize: A4
+ height: 125mm
+ horizontal_pitch: 155mm
+ source: glabel
+ left_margin: 27.5mm
+ width: 155mm
+Avery 5197:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1in
+ horizontal_pitch: 4.125in
+ height: 1.5in
+ source: glabel
+ width: 4in
+ left_margin: 0.1875in
+Dymo 99014:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 288pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 167pt
+ height: 288pt
+ source: glabel
+ width: 167pt
+ left_margin: 0pt
+Avery 5366:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 0.666666667in
+ down: 15
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4in
+ height: 0.666666667in
+ source: glabel
+ width: 3.4375in
+ left_margin: 0.53125in
+PEARL VM-5040, VM-5540:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 65mm
+ down: 4
+ papersize: A4
+ top_margin: 22.5mm
+ horizontal_pitch: 55mm
+ height: 55mm
+ source: glabel
+ width: 45mm
+ left_margin: 27mm
+Worldlabel WL-125:
+ across: 2
+ inner_margin: 0.125in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 4.1875in
+ height: 2in
+ source: glabel
+ width: 4in
+ left_margin: 0.1625in
+Tough-Tags TTLW-2016:
+ across: 5
+ inner_margin: 5pt
+ vertical_pitch: 45.3pt
+ down: 17
+ papersize: Letter
+ top_margin: 4.17pt
+ horizontal_pitch: 100.8pt
+ height: 36pt
+ source: glabel
+ width: 92.16pt
+ left_margin: 68pt
+DECAdry DLW-1786:
+ across: 2
+ inner_margin: 3.175mm
+ vertical_pitch: 39mm
+ down: 7
+ papersize: A4
+ top_margin: 12mm
+ horizontal_pitch: 105mm
+ height: 39mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Viking 02204:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 120pt
+ down: 7
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 198pt
+ height: 120pt
+ source: glabel
+ width: 198pt
+ left_margin: 0pt
+Avery 5395:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2.5in
+ down: 4
+ papersize: Letter
+ top_margin: 0.583333333in
+ horizontal_pitch: 3.75in
+ height: 2.333333333in
+ source: glabel
+ width: 3.375in
+ left_margin: 0.6875in
+Zweckform 32015:
+ across: 2
+ inner_margin: 5mm
+ vertical_pitch: 60mm
+ down: 4
+ top_margin: 31.5mm
+ papersize: A4
+ height: 54mm
+ horizontal_pitch: 91mm
+ source: glabel
+ left_margin: 17mm
+ width: 85mm
+Avery 6121:
+ across: 5
+ inner_margin: 8.496pt
+ vertical_pitch: 60.12pt
+ down: 13
+ papersize: A4
+ top_margin: 29.736pt
+ horizontal_pitch: 107.928pt
+ height: 60.12pt
+ source: glabel
+ width: 107.928pt
+ left_margin: 26.928pt
+PEARL VM-6102, VM-6502:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 65mm
+ down: 4
+ papersize: A4
+ top_margin: 23.5mm
+ horizontal_pitch: 55mm
+ height: 55mm
+ source: glabel
+ width: 45mm
+ left_margin: 27mm
+Sigel LP 800:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 55.0mm
+ down: 5
+ papersize: A4
+ top_margin: 10.0mm
+ horizontal_pitch: 95.0mm
+ height: 55.0mm
+ source: glabel
+ width: 85.0mm
+ left_margin: 15.0mm
+Zweckform J8435B:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 122mm
+ down: 1
+ top_margin: 21mm
+ papersize: A4
+ height: 122mm
+ horizontal_pitch: 155mm
+ source: glabel
+ left_margin: 27.5mm
+ width: 155mm
+Zweckform 4746:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 56.6929pt
+ down: 13
+ papersize: A4
+ top_margin: 52.2425pt
+ horizontal_pitch: 416.6pt
+ height: 56.6929pt
+ source: glabel
+ width: 416.6pt
+ left_margin: 88.8377pt
+PEARL PE-8007, PE-8017:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+DECAdry DLW-1787:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 35mm
+ down: 8
+ papersize: A4
+ top_margin: 8.5mm
+ horizontal_pitch: 70mm
+ height: 35mm
+ source: glabel
+ width: 70mm
+ left_margin: 0mm
+Avery 5997-Face:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 133pt
+ down: 5
+ papersize: Letter
+ top_margin: 60.5pt
+ horizontal_pitch: 236pt
+ height: 133pt
+ source: glabel
+ width: 220pt
+ left_margin: 80pt
+APLI 01293:
+ across: 3
+ inner_margin: 0pt
+ vertical_pitch: 94.3937pt
+ down: 8
+ papersize: A4
+ top_margin: 43.3701pt
+ horizontal_pitch: 198.425pt
+ height: 94.3937pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 0pt
+Sattleford VM-5040, VM-5540:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 64.5mm
+ down: 4
+ papersize: A4
+ top_margin: 24.5mm
+ horizontal_pitch: 55mm
+ height: 54.5mm
+ source: glabel
+ width: 45mm
+ left_margin: 28mm
+Sigel LP 801:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 55.0mm
+ down: 5
+ top_margin: 10.0mm
+ papersize: A4
+ height: 55.0mm
+ horizontal_pitch: 95.0mm
+ source: glabel
+ left_margin: 15.0mm
+ width: 85.0mm
+Avery 6490:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 2.78125in
+ height: 2in
+ source: glabel
+ width: 2.6875in
+ left_margin: 0.125in
+Herma 5090:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 133.228pt
+ down: 6
+ top_margin: 32.5984pt
+ papersize: A4
+ height: 110.551pt
+ horizontal_pitch: 544.252pt
+ source: glabel
+ left_margin: 25.5118pt
+ width: 544.252pt
+Herma 8839:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ top_margin: 60.9448pt
+ papersize: A4
+ height: 144pt
+ horizontal_pitch: 270.425pt
+ source: glabel
+ left_margin: 60.9448pt
+ width: 198.425pt
+Avery 8195:
+ across: 4
+ inner_margin: 0.125in
+ vertical_pitch: 0.666in
+ down: 15
+ papersize: Letter
+ top_margin: 0.531in
+ horizontal_pitch: 2.031in
+ height: 0.666in
+ source: glabel
+ width: 1.75in
+ left_margin: 0.297in
+Sigel LP 802:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 55.0mm
+ down: 5
+ top_margin: 10.0mm
+ papersize: A4
+ height: 55.0mm
+ horizontal_pitch: 95.0mm
+ source: glabel
+ left_margin: 15.0mm
+ width: 85.0mm
+Herma 5091:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 133.228pt
+ down: 6
+ top_margin: 32.5984pt
+ papersize: A4
+ height: 110.551pt
+ horizontal_pitch: 544.252pt
+ source: glabel
+ left_margin: 25.5118pt
+ width: 544.252pt
+Avery 7664:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 72mm
+ down: 4
+ papersize: A4
+ top_margin: 5mm
+ horizontal_pitch: 104.5mm
+ height: 71.9mm
+ source: glabel
+ width: 70mm
+ left_margin: 17mm
+PEARL VM-6083, VM-6583:
+ across: 7
+ inner_margin: 2mm
+ vertical_pitch: 16.9mm
+ down: 16
+ papersize: A4
+ top_margin: 13mm
+ horizontal_pitch: 27.9mm
+ height: 16.9mm
+ source: glabel
+ width: 25.4mm
+ left_margin: 9mm
+Bruna 241301:
+ across: 3
+ inner_margin: 9pt
+ vertical_pitch: 102.047pt
+ down: 8
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 198.425pt
+ height: 102.047pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 0pt
+Herma 5092:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 107.716pt
+ down: 7
+ top_margin: 42.5197pt
+ papersize: A4
+ height: 107.716pt
+ horizontal_pitch: 544.252pt
+ source: glabel
+ left_margin: 25.5118pt
+ width: 544.252pt
+Worldlabel WL-157:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.13in
+ down: 9
+ papersize: Letter
+ top_margin: 0.51125in
+ horizontal_pitch: 4.0674in
+ height: 0.9375in
+ source: glabel
+ width: 3.4375in
+ left_margin: 0.49755in
+PEARL VM-6233, VM-6733:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 74mm
+ down: 4
+ papersize: A4
+ top_margin: 0.5mm
+ horizontal_pitch: 105mm
+ height: 74mm
+ source: glabel
+ width: 105mm
+ left_margin: 0mm
+Herma 4620:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 105pt
+ down: 8
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 298pt
+ height: 105pt
+ source: glabel
+ width: 298pt
+ left_margin: 0pt
+Avery 38931:
+ across: 3
+ inner_margin: 3mm
+ vertical_pitch: 25mm
+ down: 11
+ papersize: A4
+ top_margin: 10mm
+ horizontal_pitch: 70mm
+ height: 25mm
+ source: glabel
+ width: 70mm
+ left_margin: 0mm
+DECAdry DLW-1731:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 108.0pt
+ down: 7
+ top_margin: 43.9pt
+ papersize: A4
+ height: 108.0pt
+ horizontal_pitch: 187.2pt
+ source: glabel
+ left_margin: 21.2pt
+ width: 181.4pt
+Avery L4770:
+ across: 4
+ inner_margin: 1mm
+ vertical_pitch: 25.4mm
+ down: 10
+ papersize: A4
+ top_margin: 22mm
+ horizontal_pitch: 48.5mm
+ height: 25.4mm
+ source: glabel
+ width: 45.7mm
+ left_margin: 10mm
+Herma 4283:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 133.228pt
+ down: 6
+ papersize: A4
+ top_margin: 32.5984pt
+ horizontal_pitch: 544.252pt
+ height: 110.551pt
+ source: glabel
+ width: 544.252pt
+ left_margin: 25.5118pt
+PEARL VM-5221, VM-5721:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 70mm
+ down: 4
+ papersize: A4
+ top_margin: 8.5mm
+ horizontal_pitch: 70mm
+ height: 70mm
+ source: glabel
+ width: 70mm
+ left_margin: 0mm
+Worldlabel WL-1125:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 133pt
+ down: 5
+ papersize: Letter
+ top_margin: 60.5pt
+ horizontal_pitch: 236pt
+ height: 133pt
+ source: glabel
+ width: 220pt
+ left_margin: 80pt
+Worldlabel WL-100:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.333333333in
+ down: 7
+ papersize: Letter
+ top_margin: 0.833333333in
+ horizontal_pitch: 4.1875in
+ height: 1.333333333in
+ source: glabel
+ width: 4in
+ left_margin: 0.15625in
+Herma 4284:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 197.008pt
+ down: 4
+ papersize: A4
+ top_margin: 38.2677pt
+ horizontal_pitch: 544.252pt
+ height: 174.331pt
+ source: glabel
+ width: 544.252pt
+ left_margin: 25.5118pt
+Herma 5095:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 197.008pt
+ down: 4
+ top_margin: 38.2677pt
+ papersize: A4
+ height: 174.331pt
+ horizontal_pitch: 544.252pt
+ source: glabel
+ left_margin: 25.5118pt
+ width: 544.252pt
+Neato 69143:
+ across: 1
+ inner_margin: 0.125in
+ vertical_pitch: 9.5in
+ down: 1
+ papersize: Letter
+ top_margin: 0.75in
+ horizontal_pitch: 4.75in
+ height: 9.5in
+ source: glabel
+ width: 4.75in
+ left_margin: 1.875in
+DECAdry DLW-1734:
+ across: 2
+ inner_margin: 3.175mm
+ vertical_pitch: 67.7mm
+ down: 4
+ papersize: A4
+ top_margin: 13.1mm
+ horizontal_pitch: 101.5mm
+ height: 67.7mm
+ source: glabel
+ width: 99.1mm
+ left_margin: 4.65mm
+Avery 5371:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 2in
+ down: 5
+ papersize: Letter
+ top_margin: 0.5in
+ horizontal_pitch: 3.5in
+ height: 2in
+ source: glabel
+ width: 3.5in
+ left_margin: 0.75in
+Dymo 11355:
+ across: 1
+ inner_margin: 0pt
+ vertical_pitch: 144pt
+ down: 1
+ papersize: Other
+ top_margin: 0pt
+ horizontal_pitch: 54pt
+ height: 144pt
+ source: glabel
+ width: 54pt
+ left_margin: 0pt
+Herma 5096:
+ across: 1
+ inner_margin: 5pt
+ vertical_pitch: 197.008pt
+ down: 4
+ top_margin: 38.2677pt
+ papersize: A4
+ height: 174.331pt
+ horizontal_pitch: 544.252pt
+ source: glabel
+ left_margin: 25.5118pt
+ width: 544.252pt
+Zweckform 3669:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ papersize: A4
+ top_margin: 61pt
+ horizontal_pitch: 198.425pt
+ height: 144pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 0pt
+Avery 7414:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 147.4pt
+ down: 5
+ papersize: A4
+ top_margin: 52.44pt
+ horizontal_pitch: 255.1pt
+ height: 147.4pt
+ source: glabel
+ width: 255.1pt
+ left_margin: 42.51pt
+Avery 7160:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 108.0pt
+ down: 7
+ papersize: A4
+ top_margin: 43.9pt
+ horizontal_pitch: 187.2pt
+ height: 108.0pt
+ source: glabel
+ width: 181.4pt
+ left_margin: 21.2pt
+Your Design VM-6310, VM-6810:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 69mm
+ down: 4
+ papersize: A4
+ top_margin: 18mm
+ horizontal_pitch: 100mm
+ height: 53.5mm
+ source: glabel
+ width: 85mm
+ left_margin: 12mm
+Sattleford VM-6200:
+ across: 1
+ inner_margin: 1mm
+ vertical_pitch: 55mm
+ down: 5
+ papersize: A4
+ top_margin: 14mm
+ horizontal_pitch: 180mm
+ height: 50mm
+ source: glabel
+ width: 180mm
+ left_margin: 15mm
+PEARL VM-5081, VM 5581:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+DECAdry OLW-4786:
+ across: 2
+ inner_margin: 3.175mm
+ vertical_pitch: 39mm
+ down: 7
+ top_margin: 12mm
+ papersize: A4
+ height: 39mm
+ horizontal_pitch: 105mm
+ source: glabel
+ left_margin: 0mm
+ width: 105mm
+Herma 4625:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 119.905pt
+ down: 7
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 297.638pt
+ height: 119.905pt
+ source: glabel
+ width: 297.638pt
+ left_margin: 0pt
+Zweckform 4780:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 72pt
+ down: 10
+ papersize: A4
+ top_margin: 53.8898pt
+ horizontal_pitch: 137.48pt
+ height: 72pt
+ source: glabel
+ width: 137.48pt
+ left_margin: 23pt
+Worldlabel WL-160:
+ across: 3
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 2.69in
+ height: 1.25in
+ source: glabel
+ width: 2.375in
+ left_margin: 0.3725in
+OfficeMax 86112:
+ across: 1
+ inner_margin: 9pt
+ vertical_pitch: 148.5pt
+ down: 3
+ papersize: Other
+ top_margin: 3.6pt
+ horizontal_pitch: 288pt
+ height: 144pt
+ source: glabel
+ width: 288pt
+ left_margin: 4.5pt
+Staples STAP14:
+ across: 2
+ inner_margin: 3mm
+ vertical_pitch: 38mm
+ down: 7
+ papersize: A4
+ top_margin: 15mm
+ horizontal_pitch: 102mm
+ height: 38mm
+ source: glabel
+ width: 99mm
+ left_margin: 5mm
+DECAdry OLW-4787:
+ across: 3
+ inner_margin: 3.2mm
+ vertical_pitch: 35mm
+ down: 8
+ top_margin: 8.5mm
+ papersize: A4
+ height: 35mm
+ horizontal_pitch: 70mm
+ source: glabel
+ left_margin: 0mm
+ width: 70mm
+Avery 8651:
+ across: 5
+ inner_margin: 5pt
+ vertical_pitch: 60.09pt
+ down: 13
+ papersize: A4
+ top_margin: 30.90pt
+ horizontal_pitch: 115.09pt
+ height: 60.09pt
+ source: glabel
+ width: 108pt
+ left_margin: 13.32pt
+Dataline 57125:
+ across: 2
+ inner_margin: 0mm
+ vertical_pitch: 55.625mm
+ down: 5
+ papersize: A4
+ top_margin: 11mm
+ horizontal_pitch: 91mm
+ height: 54.00mm
+ source: glabel
+ width: 86.00mm
+ left_margin: 16mm
+Avery 7162:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 96.1pt
+ down: 8
+ papersize: A4
+ top_margin: 36.8pt
+ horizontal_pitch: 290.5pt
+ height: 96.1pt
+ source: glabel
+ width: 280.9pt
+ left_margin: 11.3pt
+Zweckform 4781:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 119.905pt
+ down: 6
+ papersize: A4
+ top_margin: 61.030pt
+ horizontal_pitch: 274.960pt
+ height: 119.905pt
+ source: glabel
+ width: 274.960pt
+ left_margin: 22.677pt
+Worldlabel WL-161:
+ across: 2
+ inner_margin: 0.0625in
+ vertical_pitch: 1.5in
+ down: 6
+ papersize: Letter
+ top_margin: 1.125in
+ horizontal_pitch: 4in
+ height: 1.25in
+ source: glabel
+ width: 3.75in
+ left_margin: 0.375in
+Ednet 45021:
+ across: 3
+ inner_margin: 0pt
+ vertical_pitch: 104.882pt
+ down: 8
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 198.425pt
+ height: 104.882pt
+ source: glabel
+ width: 198.425pt
+ left_margin: 0pt
+Sattleford VM-6356, VM-5856:
+ across: 1
+ inner_margin: 1mm
+ vertical_pitch: 54mm
+ down: 5
+ papersize: A4
+ top_margin: 14mm
+ horizontal_pitch: 170mm
+ height: 54mm
+ source: glabel
+ width: 170mm
+ left_margin: 20mm
+Sattleford VM-6059, VM-6559:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 54mm
+ down: 5
+ papersize: A4
+ top_margin: 14mm
+ horizontal_pitch: 95mm
+ height: 54mm
+ source: glabel
+ width: 85mm
+ left_margin: 16mm
+DataBecker 0598:
+ across: 2
+ inner_margin: 9pt
+ vertical_pitch: 119.906pt
+ down: 6
+ papersize: A4
+ top_margin: 65.1968pt
+ horizontal_pitch: 274.961pt
+ height: 119.906pt
+ source: glabel
+ width: 274.961pt
+ left_margin: 23.2441pt
+Avery 7163:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 108pt
+ down: 7
+ papersize: A4
+ top_margin: 43pt
+ horizontal_pitch: 292pt
+ height: 108pt
+ source: glabel
+ width: 280.9pt
+ left_margin: 9.5pt
+Sigel DP 930:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 156pt
+ down: 5
+ top_margin: 31pt
+ papersize: A4
+ height: 155.9pt
+ horizontal_pitch: 269pt
+ source: glabel
+ left_margin: 40pt
+ width: 240.9pt
+Herma 5070:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 131.811pt
+ down: 6
+ top_margin: 24.9449pt
+ papersize: A4
+ height: 131.811pt
+ horizontal_pitch: 295.086pt
+ source: glabel
+ left_margin: 38.5512pt
+ width: 223.086pt
+Zweckform 6091:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 95.881pt
+ down: 8
+ papersize: A4
+ top_margin: 37.502pt
+ horizontal_pitch: 183.118pt
+ height: 95.811pt
+ source: glabel
+ width: 183.118pt
+ left_margin: 22.961pt
+Zweckform 3474:
+ across: 3
+ inner_margin: 0pt
+ vertical_pitch: 37mm
+ down: 8
+ papersize: A4
+ top_margin: 0pt
+ horizontal_pitch: 70mm
+ height: 37mm
+ source: glabel
+ width: 70mm
+ left_margin: 0pt
+PEARL VM-5217, VM-5517:
+ across: 2
+ inner_margin: 3.2mm
+ vertical_pitch: 130mm
+ down: 2
+ papersize: A4
+ top_margin: 14mm
+ horizontal_pitch: 107mm
+ height: 130mm
+ source: glabel
+ width: 100mm
+ left_margin: 4.5mm
+"Netc 749303-70001 ":
+ across: 3
+ inner_margin: 18pt
+ vertical_pitch: 72pt
+ down: 10
+ papersize: Letter
+ top_margin: 45pt
+ horizontal_pitch: 189pt
+ height: 59.4pt
+ source: glabel
+ width: 162pt
+ left_margin: 36pt
+Epson S041144:
+ across: 4
+ inner_margin: 5pt
+ vertical_pitch: 79.5pt
+ down: 4
+ papersize: A6
+ top_margin: 59pt
+ horizontal_pitch: 59.5pt
+ height: 68pt
+ source: glabel
+ width: 48pt
+ left_margin: 33pt
+Celcast IJ37:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 147.5pt
+ down: 5
+ papersize: A4
+ top_margin: 52.7pt
+ horizontal_pitch: 255pt
+ height: 147.5pt
+ source: glabel
+ width: 255pt
+ left_margin: 43pt
+Encre.com BCV10:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 157.32pt
+ down: 5
+ papersize: A4
+ top_margin: 42.50pt
+ horizontal_pitch: 255.12pt
+ height: 153.07pt
+ source: glabel
+ width: 243.78pt
+ left_margin: 48.19pt
+Avery 7164:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 204.038pt
+ down: 4
+ papersize: A4
+ top_margin: 10pt
+ horizontal_pitch: 187.08pt
+ height: 204.038pt
+ source: glabel
+ width: 180pt
+ left_margin: 20.84pt
+DECAdry OLW-4731:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 108.0pt
+ down: 7
+ top_margin: 43.9pt
+ papersize: A4
+ height: 108.0pt
+ horizontal_pitch: 187.2pt
+ source: glabel
+ left_margin: 21.2pt
+ width: 181.4pt
+Zweckform 3475:
+ across: 3
+ inner_margin: 0pt
+ vertical_pitch: 102.121pt
+ down: 8
+ papersize: A4
+ top_margin: 12.566pt
+ horizontal_pitch: 198.570pt
+ height: 102.121pt
+ source: glabel
+ width: 198.570pt
+ left_margin: 0pt
+PEARL VM-6097, VM-6597:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 210mm
+ down: 1
+ papersize: A5
+ top_margin: 0mm
+ horizontal_pitch: 148mm
+ height: 210mm
+ source: glabel
+ width: 148mm
+ left_margin: 0mm
+PEARL PE-8350, PE-8352:
+ across: 1
+ inner_margin: 3.2mm
+ vertical_pitch: 297mm
+ down: 1
+ papersize: A4
+ top_margin: 0mm
+ horizontal_pitch: 210mm
+ height: 297mm
+ source: glabel
+ width: 210mm
+ left_margin: 0mm
+Southworth BC:
+ across: 2
+ inner_margin: 5pt
+ vertical_pitch: 144pt
+ down: 5
+ papersize: Letter
+ top_margin: 36pt
+ horizontal_pitch: 288pt
+ height: 144pt
+ source: glabel
+ width: 252pt
+ left_margin: 36pt
+Avery 7165:
+ across: 2
+ inner_margin: 5.66929pt
+ vertical_pitch: 191.991pt
+ down: 4
+ papersize: A4
+ top_margin: 36.9638pt
+ horizontal_pitch: 288.113pt
+ height: 191.991pt
+ source: glabel
+ width: 280.8pt
+ left_margin: 13.2378pt
+Avery 8435A:
+ across: 1
+ inner_margin: 2mm
+ vertical_pitch: 125mm
+ down: 1
+ papersize: A4
+ top_margin: 22mm
+ horizontal_pitch: 155mm
+ height: 125mm
+ source: glabel
+ width: 155mm
+ left_margin: 27.5mm
+Ascom A4/24/MKII:
+ across: 3
+ inner_margin: 5pt
+ vertical_pitch: 96pt
+ down: 8
+ papersize: A4
+ top_margin: 36pt
+ horizontal_pitch: 187pt
+ height: 96pt
+ source: glabel
+ width: 181pt
+ left_margin: 19pt
diff --git a/vendored-plugins/openproject-backlogs/config/locales/de.yml b/vendored-plugins/openproject-backlogs/config/locales/de.yml
new file mode 100644
index 0000000000..4ab804aeeb
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/de.yml
@@ -0,0 +1,144 @@
+de:
+ activerecord:
+ attributes:
+ work_package:
+ position: Position
+ remaining_hours: Verbleibende Stunden
+ story_points: Story Punkte
+ backlogs_work_package_type: Backlog Typ
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: kann nur die IDs von Arbeitspaketen des aktuellen Sprints enthalten.
+ must_block_at_least_one_work_package: muss die ID von mindestens einem Arbeitspaket enthalten.
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: muss der Version der Story entsprechen.
+ parent_id:
+ parent_child_relationship_across_projects: "ist ungültig da '%{work_package_name}' ein backlog Task ist und daher kein Eltern-Arbeitspaket außerhalb des jetzigen Projekts haben kann."
+ type_must_be_one_of_the_following: 'muss einer der folgenden Typen sein: %{type_names}.'
+ sprint:
+ cannot_end_before_it_starts: Sprint kann nicht enden, bevor er begonnen hat.
+ backlogs:
+ add_new_story: Neue Story
+ any: beliebig
+ backlog_settings: Backlog Einstellungen
+ burndown_graph: Burndown Graph
+ card_paper_size: Format für Kartendruck
+ chart_options: Chart-Optionen
+ close: Schließen
+ column_width: 'Spaltenbreite:'
+ date: Tag
+ definition_of_done: Definition of Done
+ generating_chart: Generiere Graph...
+ hours: Stunden
+ impediment: Hindernis
+ label_versions_default_fold_state: Zeige Versionen eingeklappt
+ work_package_is_closed: Arbeitspaket ist abgeschlossen, wenn
+ label_is_done_status: 'Status %{status_name} bedeutet abgeschlossen'
+ no_burndown_data: 'Keine Burndown Graphen verfügbar. Start- und Enddaten der Sprints müssen definiert sein.'
+ points: Punkte
+ positions_could_not_be_rebuilt: Positionen konnten nicht neu berechnet werden.
+ positions_rebuilt_successfully: Positionen wurden neu berechnet.
+ properties: Eigenschaften
+ rebuild: Neu berechnen
+ rebuild_positions: Positionen neu berechnen
+ remaining_hours: Verbleibende Stunden
+ remaining_hours_ideal: Verbleibende Stunden (ideal)
+ show_burndown_chart: Burndown-Chart
+ story: Story
+ story_points: Story Punkte
+ story_points_ideal: Story Points (ideal)
+ task: Aufgabe
+ task_color: Farbe für Aufgaben
+ unassigned: Nicht zugewiesen
+ x_more: '%{count} mehr...'
+ backlogs_active: aktiv
+ backlogs_any: beliebig
+ backlogs_card_specification: Etikettenart für den Kartendruck
+ backlogs_inactive: Projekt zeigt keine Aktivität
+ backlogs_points_burn_direction: Burnup/-down Punkte
+ backlogs_product_backlog: Produkt Backlog
+ backlogs_product_backlog_is_empty: Produkt Backlog ist leer
+ backlogs_product_backlog_unsized: Die Spitze des Produkt Backlogs hat Stories ohne Aufwandsangaben
+ backlogs_sizing_inconsistent: Story Aufwand weicht von den Schätzwerten ab
+ backlogs_sprint_notes_missing: Geschlossene Sprints ohne Closed sprints without Retrospective-/Besprechungsnotizen
+ backlogs_sprint_unestimated: Geschlossene oder aktive Sprints mit nicht abgeschätzten Stories
+ backlogs_sprint_unsized: Das Projekt hat Stories auf aktiven oder vor kurzem geschlossenen Sprints welche keine Aufwandsangaben enthalten
+ backlogs_sprints: Sprints
+ backlogs_story: Story
+ backlogs_story_type: Story-Typ
+ backlogs_task: Aufgabe
+ backlogs_task_type: Aufgaben-Typ
+ backlogs_velocity_missing: Es konnte keine Geschwindigkeit für dieses Projekt berechnet werden
+ backlogs_velocity_varies: Die Geschwindigkeit variiert stark zwischen den Sprints
+ backlogs_wiki_template: Vorlage für das Sprint-Wiki
+ button_edit_wiki: Wiki Seite bearbeiten
+ error_intro_plural: 'Die folgenden Fehler sind aufgetreten:'
+ error_intro_singular: 'Der folgende Fehler ist aufgetreten:'
+ error_outro: Bitte beheben Sie die obigen Fehler bevor Sie erneut abschicken.
+ event_sprint_description: |
+ %{summary}: %{url}
+ %{description}
+ event_sprint_summary: '%{project}: %{summary}'
+ ideal: Ideal
+ inclusion: ist nicht in der Liste enthalten
+ label_back_to_project: Zurück zur Projektseite
+ label_backlog: Backlog
+ label_backlogs: Backlogs
+ label_backlogs_unconfigured: 'Sie haben noch keine Backlogs konfiguriert. Bitte gehen Sie auf %{administration} > %{plugins}, klicken Sie dann auf den %{configure} Link für dieses Plugin. Kommen Sie hierher zurück, sobald sie die Felder konfiguriert haben.'
+ label_blocks_ids: IDs der blockierten Arbeitspakete
+ label_burndown: Burndown
+ label_column_in_backlog: Spalte im Backlog
+ label_hours: Stunden
+ label_work_package_hierarchy: Arbeitspaketehierarchie
+ label_master_backlog: Master Backlog
+ label_not_prioritized: nicht priorisiert
+ label_points: Punkte
+ label_points_burn_down: Runter
+ label_points_burn_up: Hoch
+ label_product_backlog: Produkt Backlog
+ label_select_all: Alle auswählen
+ label_sprint_backlog: Sprint Backlog
+ label_sprint_cards: Karten exportieren
+ label_sprint_impediments: Sprint Hindernisse
+ label_sprint_name: 'Sprint "%{name}"'
+ label_sprint_velocity: 'Geschwindigkeit %{velocity}, basiert auf %{sprints} Sprints mit einem Durchschnitt von %{days} Tagen'
+ label_stories: Stories
+ label_stories_tasks: Stories/Aufgaben
+ label_task_board: Taskboard
+ label_version_setting: Versionen
+ label_webcal: Webcal Feed
+ label_wiki: Wiki
+ permission_view_master_backlog: Master Backlog ansehen
+ permission_view_taskboards: Taskboard ansehen
+ permission_update_sprints: Sprints bearbeiten
+ permission_create_stories: Stories anlegen
+ permission_update_stories: Stories bearbeiten
+ permission_create_tasks: Aufgaben anlegen
+ permission_update_tasks: Aufgaben bearbeiten
+ permission_create_impediments: Hindernisse anlegen
+ permission_update_impediments: Hindernisse bearbeiten
+ points_accepted: Akzeptierte Punkte
+ points_committed: Punkte abgeschlossen
+ points_resolved: Beschlossene Punkte
+ points_to_accept: Nicht akzeptierte Punkte
+ points_to_resolve: Nicht beschlossene Punkte
+ project_module_backlogs: Backlogs
+ rb_label_copy_tasks: Arbeitspakete kopieren
+ rb_label_copy_tasks_all: Alle
+ rb_label_copy_tasks_none: Keine
+ rb_label_copy_tasks_open: Offene
+ rb_label_link_to_original: Link zur Original-Story einfügen
+ remaining_hours: Verbleibende Stunden
+ required_burn_rate_hours: benötigte Burn-Rate (Stunden)
+ required_burn_rate_points: benötigte Burn-Rate (Punkte)
+ todo_work_package_description: |
+ %{summary}: %{url}
+ %{description}
+ todo_work_package_summary: '%{type}: %{summary}'
+ version_settings_display_label: Spalte im Backlog
+ version_settings_display_option_left: links
+ version_settings_display_option_none: keine
+ version_settings_display_option_right: rechts
diff --git a/vendored-plugins/openproject-backlogs/config/locales/en.yml b/vendored-plugins/openproject-backlogs/config/locales/en.yml
new file mode 100644
index 0000000000..ee5736075b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/en.yml
@@ -0,0 +1,193 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+---
+en:
+ activerecord:
+ attributes:
+ work_package:
+ position: "Position"
+ remaining_hours: "Remaining Hours"
+ story_points: "Story Points"
+ backlogs_work_package_type: "Backlog type"
+
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: "can only contain IDs of work packages in the current sprint."
+ must_block_at_least_one_work_package: "must contain the ID of at least one ticket."
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: "must be the same as the story's version."
+ parent_id:
+ parent_child_relationship_across_projects: "is invalid because the work package '%{work_package_name}' is a backlog task and therefore cannot have a parent outside of the current project."
+ type_must_be_one_of_the_following: "Type must be one of the following: %{type_names}."
+ sprint:
+ cannot_end_before_it_starts: "Sprint cannot end before it starts."
+
+ backlogs:
+ add_new_story: "New Story"
+ any: "any"
+ backlog_settings: "Backlog Settings"
+ burndown_graph: "Burndown Graph"
+ card_paper_size: "Paper size for card printing"
+ chart_options: "Chart options"
+ close: "Close"
+ column_width: "Column width:"
+ date: "Day"
+ definition_of_done: "Definition of Done"
+ generating_chart: "Generating Graph..."
+ hours: "Hours"
+ impediment: "Impediment"
+ label_versions_default_fold_state: "Show versions folded"
+ work_package_is_closed: "Work package is done, when"
+ label_is_done_status: "Status %{status_name} means done"
+ no_burndown_data: "No burndown data available. It is necessary to have the sprint start- and end dates set."
+ points: "Points"
+ positions_could_not_be_rebuilt: "Positions could not be rebuilt."
+ positions_rebuilt_successfully: "Positions rebuilt successfully."
+ properties: "Properties"
+ rebuild: "Rebuild"
+ rebuild_positions: "Rebuild positions"
+ remaining_hours: "Remaining hours"
+ remaining_hours_ideal: "Remaining hours (ideal)"
+ show_burndown_chart: "Burndown Chart"
+ story: "Story"
+ story_points: "Story Points"
+ story_points_ideal: "Story Points (ideal)"
+ task: "Task"
+ task_color: "Task color"
+ unassigned: "Unassigned"
+ x_more: "%{count} more..."
+
+ backlogs_active: "active"
+ backlogs_any: "any"
+ backlogs_card_specification: "Label types for card printing"
+ backlogs_inactive: "Project shows no activity"
+ backlogs_points_burn_direction: "Points burn up/down"
+ backlogs_product_backlog: "Product backlog"
+ backlogs_product_backlog_is_empty: "Product backlog is empty"
+ backlogs_product_backlog_unsized: "The top of the product backlog has unsized stories"
+ backlogs_sizing_inconsistent: "Story sizes vary against their estimates"
+ backlogs_sprint_notes_missing: "Closed sprints without retrospective/review notes"
+ backlogs_sprint_unestimated: "Closed or active sprints with unestimated stories"
+ backlogs_sprint_unsized: "Project has stories on active or recently closed sprints that were not sized"
+ backlogs_sprints: "Sprints"
+ backlogs_story: "Story"
+ backlogs_story_type: "Story types"
+ backlogs_task: "Task"
+ backlogs_task_type: "Task type"
+ backlogs_velocity_missing: "No velocity could be calculated for this project"
+ backlogs_velocity_varies: "Velocity varies significantly over sprints"
+ backlogs_wiki_template: "Template for sprint wiki page"
+
+ button_edit_wiki: "Edit wiki page"
+
+ error_intro_plural: "The following errors were encountered:"
+ error_intro_singular: "The following error was encountered:"
+ error_outro: "Please correct the above errors before submitting again."
+
+ event_sprint_description: "%{summary}: %{url}\n%{description}"
+ event_sprint_summary: "%{project}: %{summary}"
+
+ ideal: "ideal"
+
+ inclusion: "is not included in the list"
+
+ label_back_to_project: "Back to project page"
+ label_backlog: "Backlog"
+ label_backlogs: "Backlogs"
+ label_backlogs_unconfigured: "You have not configured Backlogs yet. Please go to %{administration} > %{plugins}, then click on the %{configure} link for this plugin. Once you have set the fields, come back to this page to start using the tool."
+ label_blocks_ids: "IDs of blocked work packages"
+ label_burndown: "Burndown"
+ label_column_in_backlog: "Column in backlog"
+ label_hours: "hours"
+ label_work_package_hierarchy: "Work package Hierarchy"
+ label_master_backlog: "Master Backlog"
+ label_not_prioritized: "not prioritized"
+ label_points: "points"
+ label_points_burn_down: "Down"
+ label_points_burn_up: "Up"
+ label_product_backlog: "product backlog"
+ label_select_all: "Select all"
+ label_sprint_backlog: "sprint backlog"
+ label_sprint_cards: "Export cards"
+ label_sprint_impediments: "Sprint Impediments"
+ label_sprint_name: "Sprint \"%{name}\""
+ label_sprint_velocity: "Velocity %{velocity}, based on %{sprints} sprints with an average %{days} days"
+ label_stories: "Stories"
+ label_stories_tasks: "Stories/Tasks"
+ label_task_board: "Task board"
+ label_version_setting: "Versions"
+ label_webcal: "Webcal Feed"
+ label_wiki: "Wiki"
+
+ permission_view_master_backlog: "View master backlog"
+ permission_view_taskboards: "View taskboards"
+ permission_update_sprints: "Update sprints"
+ permission_create_stories: "Create stories"
+ permission_update_stories: "Update stories"
+ permission_create_tasks: "Create tasks"
+ permission_update_tasks: "Update tasks"
+ permission_create_impediments: "Create impediments"
+ permission_update_impediments: "Update impediments"
+
+ points_accepted: "points accepted"
+ points_committed: "points committed"
+ points_resolved: "points resolved"
+ points_to_accept: "points not accepted"
+ points_to_resolve: "points not resolved"
+
+ project_module_backlogs: "Backlogs"
+
+ rb_label_copy_tasks: "Copy work packages"
+ rb_label_copy_tasks_all: "All"
+ rb_label_copy_tasks_none: "None"
+ rb_label_copy_tasks_open: "Open"
+ rb_label_link_to_original: "Include link to original story"
+
+ remaining_hours: "remaining hours"
+
+ required_burn_rate_hours: "required burn rate (hours)"
+ required_burn_rate_points: "required burn rate (points)"
+
+ todo_work_package_description: "%{summary}: %{url}\n%{description}"
+ todo_work_package_summary: "%{type}: %{summary}"
+
+ version_settings_display_label: "Column in backlog"
+ version_settings_display_option_left: "left"
+ version_settings_display_option_none: "none"
+ version_settings_display_option_right: "right"
diff --git a/vendored-plugins/openproject-backlogs/config/locales/fr.yml b/vendored-plugins/openproject-backlogs/config/locales/fr.yml
new file mode 100644
index 0000000000..73d487d289
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/fr.yml
@@ -0,0 +1,144 @@
+fr:
+ activerecord:
+ attributes:
+ work_package:
+ position: position
+ remaining_hours: Heures restantes
+ story_points: "Points d'histoire"
+ backlogs_work_package_type: Type de backlog
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: ne peut contenir que des ID de lots de travaux du sprint actuel.
+ must_block_at_least_one_work_package: "doit contenir l'ID d'au moins un ticket."
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: "doit être identique à la version de l'histoire."
+ parent_id:
+ parent_child_relationship_across_projects: "n'est pas valide parce que le lot de travaux «%{work_package_name}» est une tâche de backlog et par conséquent ne peut avoir un parent en dehors du projet actuel."
+ type_must_be_one_of_the_following: 'Type doit être parmi : %{type_names}.'
+ sprint:
+ cannot_end_before_it_starts: "Un sprint ne peut pas se terminer avant d'avoir débuté."
+ backlogs:
+ add_new_story: Nouvelle histoire
+ any: tout
+ backlog_settings: Paramètres du backlog
+ burndown_graph: "Graphique d'avancement"
+ card_paper_size: "Format de papier pour l'impression des cartes"
+ chart_options: Chart options
+ close: Clôturer
+ column_width: 'Largeur de la colonne :'
+ date: Jour
+ definition_of_done: Définition de Fait
+ generating_chart: Génération du graphe…
+ hours: Heures
+ impediment: Obstacle
+ label_versions_default_fold_state: Afficher les versions de manière repliée
+ work_package_is_closed: Le lot de travaux est fait lorsque
+ label_is_done_status: 'Le statut %{status_name} signifie fait'
+ no_burndown_data: "Aucune donnée d'avancement disponible. Il est nécessaire que les dates de début et de fin du sprint soient définies."
+ points: Points
+ positions_could_not_be_rebuilt: "Les positions n'ont pu être reconstruites."
+ positions_rebuilt_successfully: Positions reconstruites avec succès.
+ properties: Propriétés
+ rebuild: Reconstruire
+ rebuild_positions: Reconstruire les positions
+ remaining_hours: Heures restantes
+ remaining_hours_ideal: Heures restantes (idéal)
+ show_burndown_chart: "Graphique d'avancement"
+ story: Histoire
+ story_points: "Points d'histoire"
+ story_points_ideal: "Points d'histoire (idéal)"
+ task: Tâche
+ task_color: Couleur des tâches
+ unassigned: Non assigné
+ x_more: '%{count} de plus...'
+ backlogs_active: actif
+ backlogs_any: tout
+ backlogs_card_specification: "Types d'étiquettes pour l'impression de cartes"
+ backlogs_inactive: Le projet ne montre aucune activité
+ backlogs_points_burn_direction: Les points évoluent vers le haut/bas
+ backlogs_product_backlog: Backlog de produit
+ backlogs_product_backlog_is_empty: Le backlog de produit est vide
+ backlogs_product_backlog_unsized: Le dessus du backlog de produit contient des histoires non dimensionnées
+ backlogs_sizing_inconsistent: "La taille des histoires varie à l'encontre des estimations"
+ backlogs_sprint_notes_missing: Sprints fermés sans notes de rétrospective/revue
+ backlogs_sprint_unestimated: Sprints clôturés ou actifs avec des histoires non estimées
+ backlogs_sprint_unsized: "Le projet contient des histoires sur des sprints actifs ou récemment clôturés dont la taille n'a pas été spécifiée"
+ backlogs_sprints: Sprints
+ backlogs_story: Histoire
+ backlogs_story_type: "Types d'histoire"
+ backlogs_task: Tâche
+ backlogs_task_type: Type de tâche
+ backlogs_velocity_missing: "Aucune vélocité n'a pu être calculée pour ce projet"
+ backlogs_velocity_varies: "La vélocité varie significativement d'un sprint à l'autre"
+ backlogs_wiki_template: Modèle pour page wiki de sprint
+ button_edit_wiki: Éditer la page du wiki
+ error_intro_plural: 'Les erreurs suivantes ont été rencontrées :'
+ error_intro_singular: "L'erreur suivante a été rencontrée :"
+ error_outro: Veuillez corriger les erreurs ci-dessus avant de soumettre à nouveau.
+ event_sprint_description: |
+ %{summary} : %{url}
+ %{description}
+ event_sprint_summary: '%{project} : %{summary}'
+ ideal: idéal
+ inclusion: "n'est pas inclus(e) dans la liste"
+ label_back_to_project: Retour à la page du projet
+ label_backlog: Backlog
+ label_backlogs: Backlogs
+ label_backlogs_unconfigured: "Vous n'avez pas encore configuré Backlogs. Veuillez vous rendre dans %{administration} > %{plugins}, puis cliquer sur le lien %{configure} pour ce plugin. Une fois que vous avez défini les champs, revenez sur cette page pour commencer à utiliser l'outil."
+ label_blocks_ids: ID des lots de travaux bloqués
+ label_burndown: Avancement
+ label_column_in_backlog: Colonne dans le backlog
+ label_hours: heures
+ label_work_package_hierarchy: Hiérarchie du lot de travaux
+ label_master_backlog: Backlog principal
+ label_not_prioritized: non priorisé
+ label_points: points
+ label_points_burn_down: Vers le bas
+ label_points_burn_up: Vers le haut
+ label_product_backlog: carnet de produit
+ label_select_all: Tout sélectionner
+ label_sprint_backlog: carnet de sprint
+ label_sprint_cards: Exporter les cartes
+ label_sprint_impediments: Obstacles de sprint
+ label_sprint_name: 'Sprint "%{name}"'
+ label_sprint_velocity: 'Vélocité %{velocity}, issue de %{sprints} sprints avec une moyenne de %{days} jours'
+ label_stories: Histoires
+ label_stories_tasks: Histoires/tâches
+ label_task_board: Tableau des tâches
+ label_version_setting: Versions
+ label_webcal: Flux Webcal
+ label_wiki: Wiki
+ permission_view_master_backlog: Afficher le backlog principal
+ permission_view_taskboards: Voir les tableaux des tâches
+ permission_update_sprints: Éditer les sprints
+ permission_create_stories: Créer des histoires
+ permission_update_stories: Éditer les histoires
+ permission_create_tasks: Créer des tâches
+ permission_update_tasks: Éditer les tâches
+ permission_create_impediments: Créer des obstacles
+ permission_update_impediments: Éditer les obstacles
+ points_accepted: points acceptés
+ points_committed: points soumis
+ points_resolved: points résolus
+ points_to_accept: points non acceptés
+ points_to_resolve: points non résolus
+ project_module_backlogs: Backlogs
+ rb_label_copy_tasks: Copier lots de travaux
+ rb_label_copy_tasks_all: Toutes
+ rb_label_copy_tasks_none: Aucune
+ rb_label_copy_tasks_open: Ouvertes
+ rb_label_link_to_original: "Inclure le lien vers l'histoire originale"
+ remaining_hours: heures restantes
+ required_burn_rate_hours: rythme nécessaire (heures)
+ required_burn_rate_points: rythme nécessaire (points)
+ todo_work_package_description: |
+ %{summary} : %{url}
+ %{description}
+ todo_work_package_summary: '%{type} : %{summary}'
+ version_settings_display_label: Colonne dans le backlog
+ version_settings_display_option_left: gauche
+ version_settings_display_option_none: aucune
+ version_settings_display_option_right: droite
diff --git a/vendored-plugins/openproject-backlogs/config/locales/it.yml b/vendored-plugins/openproject-backlogs/config/locales/it.yml
new file mode 100644
index 0000000000..9326ea3df0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/it.yml
@@ -0,0 +1,144 @@
+it:
+ activerecord:
+ attributes:
+ work_package:
+ position: Posizione
+ remaining_hours: Ore rimanenti
+ story_points: Punti della storia
+ backlogs_work_package_type: Tipo di backlog
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: può contenere solo ID dei pacchetti di lavoro presenti nello sprint corrente.
+ must_block_at_least_one_work_package: "deve contenere l'ID di almeno un ticket."
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: deve essere uguale alla versione della storia.
+ parent_id:
+ parent_child_relationship_across_projects: "non è valido perché il pacchetto di lavoro '%{work_package_name}' è un task di backlog e pertanto non può avere una dipendenza esterna al progetto corrente."
+ type_must_be_one_of_the_following: 'Il tipo deve essere uno dei seguenti: %{type_names}.'
+ sprint:
+ cannot_end_before_it_starts: Uno sprint non può terminare prima che venga avviato.
+ backlogs:
+ add_new_story: Nuova storia
+ any: qualsiasi
+ backlog_settings: Impostazioni di backlog
+ burndown_graph: Grafico Burndown
+ card_paper_size: Formato della carta per la stampa delle schede
+ chart_options: Opzioni grafico
+ close: Chiuso
+ column_width: 'Larghezza della colonna:'
+ date: Giorno
+ definition_of_done: Definizione di fatto
+ generating_chart: Grafico in generazione...
+ hours: Ore
+ impediment: Impedimento
+ label_versions_default_fold_state: Espandi le versioni
+ work_package_is_closed: Il pacchetto di lavoro è fatto, quando
+ label_is_done_status: 'Lo stato %{status_name} vuol dire completato'
+ no_burndown_data: Non sono disponibili i dati del burndown. È necessario avere impostato le date di inizio e fine dello sprint.
+ points: Punti
+ positions_could_not_be_rebuilt: Le posizioni non potrebbero essere ricostruite.
+ positions_rebuilt_successfully: Posizioni ricostruite correttamente.
+ properties: Proprietà
+ rebuild: Ricostruisci
+ rebuild_positions: Ricostruisce le posizioni
+ remaining_hours: Ore rimanenti
+ remaining_hours_ideal: Ore rimanenti (ideale)
+ show_burndown_chart: Grafico Burndown
+ story: Storia
+ story_points: Punti della storia
+ story_points_ideal: Punti della storia (ideale)
+ task: Attività
+ task_color: Colore attività
+ unassigned: Non assegnato
+ x_more: '%{count} più...'
+ backlogs_active: attivo
+ backlogs_any: qualsiasi
+ backlogs_card_specification: Tipi di etichette per la stampa delle schede
+ backlogs_inactive: Progetto non mostra alcuna attività
+ backlogs_points_burn_direction: Punteggi per burn positivo/negativo
+ backlogs_product_backlog: Backlog del prodotto
+ backlogs_product_backlog_is_empty: Il backlog prodotto è vuoto
+ backlogs_product_backlog_unsized: In cima al backlog di prodotto vi sono story non quantificate
+ backlogs_sizing_inconsistent: Le dimensioni della storia variano rispetto le loro stime
+ backlogs_sprint_notes_missing: Sprint chiusi senza note retrospettive/recensioni
+ backlogs_sprint_unestimated: Sprint chiusi o attivi con storie non quantificate
+ backlogs_sprint_unsized: Il progetto ha storie su sprint attivi o chiusi di recente che non sono stati quantificati
+ backlogs_sprints: Sprint
+ backlogs_story: Storia
+ backlogs_story_type: Tipi di storia
+ backlogs_task: Attività
+ backlogs_task_type: Tipo di attività
+ backlogs_velocity_missing: Per questo progetto non può essere calcolata la velocità
+ backlogs_velocity_varies: La velocità del progetto varia in modo significativo tra gli sprint
+ backlogs_wiki_template: Modello per pagina wiki dello sprint
+ button_edit_wiki: Modifica la pagina wiki
+ error_intro_plural: 'Si sono verificati i seguenti errori:'
+ error_intro_singular: 'È stato rilevato il seguente errore:'
+ error_outro: Si prega di correggere gli errori riportati prima di inviare nuovamente.
+ event_sprint_description: |
+ %{summary}: %{url}
+ %{description}
+ event_sprint_summary: '%{project}: %{summary}'
+ ideal: ideale
+ inclusion: "non è incluso nell'elenco"
+ label_back_to_project: Torna alla pagina del progetto
+ label_backlog: Backlog
+ label_backlogs: Backlog
+ label_backlogs_unconfigured: 'Non hai ancora configurato i Backlog. Vai su %{administration} > %{plugins}, quindi fai clic sul link %{configure} per il plugin. Dopo aver impostato i campi, torna su questa pagina per iniziare a utilizzare lo strumento.'
+ label_blocks_ids: ID dei pacchetti di lavoro bloccati
+ label_burndown: Burndown
+ label_column_in_backlog: Colonna nel backlog
+ label_hours: ore
+ label_work_package_hierarchy: Gerarchia dei pacchetto di lavoro
+ label_master_backlog: Master Backlog
+ label_not_prioritized: priorità non definita
+ label_points: punti
+ label_points_burn_down: Verso il basso
+ label_points_burn_up: "Verso l'alto"
+ label_product_backlog: backlog del prodotto
+ label_select_all: Seleziona tutto
+ label_sprint_backlog: backlog di sprint
+ label_sprint_cards: Esporta schede
+ label_sprint_impediments: Impedimenti allo sprint
+ label_sprint_name: 'Sprint "%{name}"'
+ label_sprint_velocity: 'La velocity %{velocity}, basato su %{sprints} Sprint di %{days} giorni in media'
+ label_stories: Storie
+ label_stories_tasks: Storie/Attività
+ label_task_board: Pannello delle attività
+ label_version_setting: Versioni
+ label_webcal: Webcal Feed
+ label_wiki: Wiki
+ permission_view_master_backlog: Visualizza il master backlog
+ permission_view_taskboards: Visualizza i pannelli delle attività
+ permission_update_sprints: Aggiorna gli sprint
+ permission_create_stories: Crea le storie
+ permission_update_stories: Aggiorna le storie
+ permission_create_tasks: Crea attività
+ permission_update_tasks: Aggiorna attività
+ permission_create_impediments: Crea impedimenti
+ permission_update_impediments: Aggiorna impedimenti
+ points_accepted: punti accettati
+ points_committed: punti acquisiti
+ points_resolved: punti risolti
+ points_to_accept: punti non accettati
+ points_to_resolve: punti non risolti
+ project_module_backlogs: Backlog
+ rb_label_copy_tasks: Copia i pacchetti di lavoro
+ rb_label_copy_tasks_all: Tutti
+ rb_label_copy_tasks_none: Nessuno
+ rb_label_copy_tasks_open: Aperti
+ rb_label_link_to_original: Include il link alla storia originale
+ remaining_hours: ore rimanenti
+ required_burn_rate_hours: burn rate richiesto (ore)
+ required_burn_rate_points: burn rate richiesto (punti)
+ todo_work_package_description: |
+ %{summary}: %{url}
+ %{description}
+ todo_work_package_summary: '%{type}: %{summary}'
+ version_settings_display_label: Colonna nel backlog
+ version_settings_display_option_left: sinistra
+ version_settings_display_option_none: nessuno
+ version_settings_display_option_right: destra
diff --git a/vendored-plugins/openproject-backlogs/config/locales/ja.yml b/vendored-plugins/openproject-backlogs/config/locales/ja.yml
new file mode 100644
index 0000000000..9f53016456
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/ja.yml
@@ -0,0 +1,144 @@
+ja:
+ activerecord:
+ attributes:
+ work_package:
+ position: 位置
+ remaining_hours: 残り時間
+ story_points: ストーリーポイント
+ backlogs_work_package_type: バックログの種類
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: では現在のスプリント内の作業項目IDのみを含めることができます。
+ must_block_at_least_one_work_package: は少なくとも 1つのチケットIDを含める必要があります。
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: はストーリーのバージョンと同じである必要があります。
+ parent_id:
+ parent_child_relationship_across_projects: "は無効です。作業項目'%{work_package_name}'はバックログタスクで、現在のプロジェクト外の親を持つことはできません。"
+ type_must_be_one_of_the_following: '型は次のいずれかである必要があります: %{type_names}'
+ sprint:
+ cannot_end_before_it_starts: スプリントは開始する前に終了できません。
+ backlogs:
+ add_new_story: 新しいストーリー
+ any: 全て
+ backlog_settings: バックログの設定
+ burndown_graph: バーンダウングラフ
+ card_paper_size: カード印刷用の用紙サイズ
+ chart_options: Chart options
+ close: 終了する
+ column_width: '列の幅:'
+ date: 日
+ definition_of_done: 「終了」の定義
+ generating_chart: グラフを生成中...
+ hours: 時間
+ impediment: 障害事項
+ label_versions_default_fold_state: バージョンを折り畳んで表示
+ work_package_is_closed: 作業パッケージが終了するには
+ label_is_done_status: 'ステータス%{status_name}は完了として扱う'
+ no_burndown_data: バーンダウンデータがありません。スプリントの開始日と終了日を設定する必要があります。
+ points: ポイント
+ positions_could_not_be_rebuilt: 位置は再構築されませんでした。
+ positions_rebuilt_successfully: 位置情報は再構築しました。
+ properties: プロパティ
+ rebuild: 再構築
+ rebuild_positions: 位置情報を再構築
+ remaining_hours: 残り時間
+ remaining_hours_ideal: 残り時間 (理想)
+ show_burndown_chart: バーンダウングラフ
+ story: ストーリー
+ story_points: ストーリーポイント
+ story_points_ideal: ストーリーポイント (理想)
+ task: タスク
+ task_color: タスクの色
+ unassigned: 未割り当て
+ x_more: '残%{count}件...'
+ backlogs_active: アクティブ
+ backlogs_any: 全て
+ backlogs_card_specification: カード印刷用ラベルの種類
+ backlogs_inactive: プロジェクトの活動がありません。
+ backlogs_points_burn_direction: ポイントのバーンアップ/バーンダウン
+ backlogs_product_backlog: プロダクトバックログ
+ backlogs_product_backlog_is_empty: プロダクトバックログはありません。
+ backlogs_product_backlog_unsized: プロダクトバックログの最上部にはサイズ化されてないストーリーがあります。
+ backlogs_sizing_inconsistent: ストーリーのサイズは見積もりと異なります。
+ backlogs_sprint_notes_missing: 反復振り返り/レビューのメモなしで閉じたスプリント
+ backlogs_sprint_unestimated: 見積もってないストーリーを持つアクティブなもしくはクローズされたスプリント
+ backlogs_sprint_unsized: プロジェクトはアクティブなもしくは最近クローズされたスプリントに対しサイズが合っていないストーリーを含んでいます
+ backlogs_sprints: スプリント
+ backlogs_story: ストーリー
+ backlogs_story_type: ストーリーの種類
+ backlogs_task: タスク
+ backlogs_task_type: タスクの種類
+ backlogs_velocity_missing: このプロジェクトのベロシティを計算できなかった
+ backlogs_velocity_varies: ベロシティがスプリントと大幅に異なります
+ backlogs_wiki_template: スプリントのWikiページのテンプレート
+ button_edit_wiki: Wikiページの編集
+ error_intro_plural: 次のエラーが発生しました:
+ error_intro_singular: 次のエラーが発生しました:
+ error_outro: 送信する前に上記のエラーを修正してください。
+ event_sprint_description: |
+ %{summary}: %{url}
+ %{description}
+ event_sprint_summary: '%{project}: %{summary}'
+ ideal: 理想時間
+ inclusion: はリストに含まれていません。
+ label_back_to_project: プロジェクトページに戻る
+ label_backlog: バックログ
+ label_backlogs: バックログ
+ label_backlogs_unconfigured: 'バックログは未設定です。%{administration} > %{plugins}をアクセスして、このプラグインの%{configure}リンクをクリックしてください。フィールドを設定した後、このページに戻ってツールを使用開始してください。'
+ label_blocks_ids: ブロックされている作業項目のID
+ label_burndown: バーンダウン
+ label_column_in_backlog: バックログの列
+ label_hours: 時間
+ label_work_package_hierarchy: 作業項目の階層
+ label_master_backlog: マスターバックログ
+ label_not_prioritized: 優先度が未設定
+ label_points: ポイント
+ label_points_burn_down: ダウン
+ label_points_burn_up: アップ
+ label_product_backlog: プロダクトバックログ
+ label_select_all: 全てを選択
+ label_sprint_backlog: スプリントバックログ
+ label_sprint_cards: カードをエクスポート
+ label_sprint_impediments: スプリント障害事項
+ label_sprint_name: 'スプリント"%{name}"'
+ label_sprint_velocity: '平均 %{days} 日 を持つ%{sprints}スプリントに基づく%{velocity}ベロシティ'
+ label_stories: ストーリー
+ label_stories_tasks: ストーリー/タスク
+ label_task_board: かんばん
+ label_version_setting: バージョン
+ label_webcal: Webcal フィード
+ label_wiki: Wiki
+ permission_view_master_backlog: マスター バックログの表示
+ permission_view_taskboards: かんばんの表示
+ permission_update_sprints: スプリントの更新
+ permission_create_stories: ストーリーの作成
+ permission_update_stories: ストーリーの更新
+ permission_create_tasks: タスクの作成
+ permission_update_tasks: タスクの更新
+ permission_create_impediments: 障害事項の作成
+ permission_update_impediments: 障害事項の更新
+ points_accepted: 進行中のポイント
+ points_committed: 承認されたポイント
+ points_resolved: 完了したポイント
+ points_to_accept: 未着手ポイント
+ points_to_resolve: 未解決ポイント
+ project_module_backlogs: バックログ
+ rb_label_copy_tasks: 作業項目をコピー
+ rb_label_copy_tasks_all: 全て
+ rb_label_copy_tasks_none: なし
+ rb_label_copy_tasks_open: 開く
+ rb_label_link_to_original: 元のストーリーへのリンクを含める
+ remaining_hours: 残り時間
+ required_burn_rate_hours: 必要なバーンレート(時間)
+ required_burn_rate_points: 必要なバーンレート(ポイント)
+ todo_work_package_description: |
+ %{summary}: %{url}
+ %{description}
+ todo_work_package_summary: '%{type}: %{summary}'
+ version_settings_display_label: バックログの列
+ version_settings_display_option_left: 左へ
+ version_settings_display_option_none: なし
+ version_settings_display_option_right: 右へ
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-de.yml b/vendored-plugins/openproject-backlogs/config/locales/js-de.yml
new file mode 100644
index 0000000000..f54e992282
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-de.yml
@@ -0,0 +1,6 @@
+de:
+ js:
+ work_packages:
+ properties:
+ storyPoints: Story Punkte
+ remainingTime: Verbleibende Stunden
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-en.yml b/vendored-plugins/openproject-backlogs/config/locales/js-en.yml
new file mode 100644
index 0000000000..77f0ea08f4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-en.yml
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+en:
+ js:
+ work_packages:
+ properties:
+ storyPoints: "Story Points"
+ remainingTime: "Remaining Hours"
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-fr.yml b/vendored-plugins/openproject-backlogs/config/locales/js-fr.yml
new file mode 100644
index 0000000000..75e7ad99c1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-fr.yml
@@ -0,0 +1,6 @@
+fr:
+ js:
+ work_packages:
+ properties:
+ storyPoints: "Points d'histoire"
+ remainingTime: Heures restantes
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-it.yml b/vendored-plugins/openproject-backlogs/config/locales/js-it.yml
new file mode 100644
index 0000000000..3eb194e7ad
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-it.yml
@@ -0,0 +1,6 @@
+it:
+ js:
+ work_packages:
+ properties:
+ storyPoints: Punti della storia
+ remainingTime: Ore rimanenti
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-ja.yml b/vendored-plugins/openproject-backlogs/config/locales/js-ja.yml
new file mode 100644
index 0000000000..6e0a024673
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-ja.yml
@@ -0,0 +1,6 @@
+ja:
+ js:
+ work_packages:
+ properties:
+ storyPoints: ストーリーポイント
+ remainingTime: 残り時間
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-pt-BR.yml b/vendored-plugins/openproject-backlogs/config/locales/js-pt-BR.yml
new file mode 100644
index 0000000000..ad9fa21365
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-pt-BR.yml
@@ -0,0 +1,6 @@
+pt-BR:
+ js:
+ work_packages:
+ properties:
+ storyPoints: Pontos de história
+ remainingTime: Horas restantes
diff --git a/vendored-plugins/openproject-backlogs/config/locales/js-sv-SE.yml b/vendored-plugins/openproject-backlogs/config/locales/js-sv-SE.yml
new file mode 100644
index 0000000000..8eec1f92c8
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/js-sv-SE.yml
@@ -0,0 +1,6 @@
+sv:
+ js:
+ work_packages:
+ properties:
+ storyPoints: Berättelsepoäng
+ remainingTime: Återstående timmar
diff --git a/vendored-plugins/openproject-backlogs/config/locales/pt-BR.yml b/vendored-plugins/openproject-backlogs/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..d918c05ed1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/pt-BR.yml
@@ -0,0 +1,140 @@
+pt-BR:
+ activerecord:
+ attributes:
+ work_package:
+ position: Posição
+ remaining_hours: Horas restantes
+ story_points: Pontos de história
+ backlogs_work_package_type: Tipo de backlog
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: pode conter somente os IDs dos pacotes de trabalho no sprint atual.
+ must_block_at_least_one_work_package: deve conter o ID de pelo menos um tíquete.
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: deve ser da mesma versão da história.
+ parent_id:
+ parent_child_relationship_across_projects: "é inválido porque o pacote de trabalho '%{work_package_name}' é uma tarefa do backlog e, portanto, não pode ter um pai fora do projeto atual."
+ type_must_be_one_of_the_following: 'Tipo deve ser um dos seguintes: %{type_names}.'
+ sprint:
+ cannot_end_before_it_starts: Sprint não pode terminar antes de começar.
+ backlogs:
+ add_new_story: Nova história
+ any: qualquer
+ backlog_settings: Configurações de backlog
+ burndown_graph: Gráfico de Burndown
+ card_paper_size: Tamanho do papel para impressão de cartões
+ chart_options: Opções de gráfico
+ close: Fechar
+ column_width: 'Largura da Coluna:'
+ date: Dia
+ definition_of_done: Definição de pronto
+ generating_chart: Gerando gráfico...
+ hours: Horas
+ impediment: Impedimento
+ label_versions_default_fold_state: Mostrar versões em modo fechado
+ work_package_is_closed: Pacote de trabalho está pronto, quando
+ label_is_done_status: 'Situação %{status_name} significa pronto'
+ no_burndown_data: Não há dados de burndown disponíveis. É necessário ter o conjunto de datas de início e fim de sprint.
+ points: Pontos
+ positions_could_not_be_rebuilt: Posições não poderiam ser reconstruídas.
+ positions_rebuilt_successfully: Posições reconstruídas com sucesso.
+ properties: Propriedades
+ rebuild: Reconstruir
+ rebuild_positions: Reconstruir posições
+ remaining_hours: Horas restantes
+ remaining_hours_ideal: Horas restantes (ideal)
+ show_burndown_chart: Gráfico de Burndown
+ story: História
+ story_points: Pontos de história
+ story_points_ideal: Pontos de história (ideal)
+ task: Tarefa
+ task_color: Cor da tarefa
+ unassigned: Não atribuída
+ x_more: '%{count} mais...'
+ backlogs_active: ativo
+ backlogs_any: qualquer
+ backlogs_card_specification: Tipos de etiqueta para impressão de cartões
+ backlogs_inactive: Projeto não mostra nenhuma atividade
+ backlogs_points_burn_direction: Pontos burn up/down
+ backlogs_product_backlog: Backlog do produto
+ backlogs_product_backlog_is_empty: Backlog do produto está vazio
+ backlogs_product_backlog_unsized: O topo do backlog de produto tem histórias não dimensionadas
+ backlogs_sizing_inconsistent: Tamanhos das histórias contrastam com suas estimativas
+ backlogs_sprint_notes_missing: Sprints fechados sem notas de revisão/retrospectiva
+ backlogs_sprint_unestimated: Sprints ativos ou fechados com histórias não estimadas
+ backlogs_sprint_unsized: Projeto tem histórias em sprints ativos ou recentemente fechados que não tem tamanho
+ backlogs_sprints: Sprints
+ backlogs_story: História
+ backlogs_story_type: Tipos de história
+ backlogs_task: Tarefa
+ backlogs_task_type: Tipo de tarefa
+ backlogs_velocity_missing: Nenhuma velocidade foi calculada para este projeto
+ backlogs_velocity_varies: Velocidade varia significativamente ao longo de sprints
+ backlogs_wiki_template: Modelo para a página de wiki da sprint
+ button_edit_wiki: Editar página wiki
+ error_intro_plural: 'Foram encontrados os seguintes erros :'
+ error_intro_singular: 'Foi encontrado o seguinte erro:'
+ error_outro: Por favor, corrija os erros acima antes de enviar novamente.
+ event_sprint_description: '%{summary}: %{url}%{description}'
+ event_sprint_summary: '%{project}: %{summary}'
+ ideal: ideal
+ inclusion: não está incluído na lista
+ label_back_to_project: Voltar à página do projeto
+ label_backlog: Backlog
+ label_backlogs: Backlogs
+ label_backlogs_unconfigured: 'Você ainda não configurou o Backlog. Por favor, vá para %{administration} > %{plugins} e, em seguida, clique em %{configure} o link para este plugin. Uma vez que você definiu os campos, volte a esta página para começar a usar a ferramenta.'
+ label_blocks_ids: IDs dos pacotes de trabalho bloqueados
+ label_burndown: Burndown
+ label_column_in_backlog: Coluna no backlog
+ label_hours: horas
+ label_work_package_hierarchy: Hierarquia de Pacote de Trabalho
+ label_master_backlog: Backlog principal
+ label_not_prioritized: não priorizado
+ label_points: pontos
+ label_points_burn_down: Abaixo
+ label_points_burn_up: Acima
+ label_product_backlog: Backlog do produto
+ label_select_all: Selecionar tudo
+ label_sprint_backlog: Backlog do sprint
+ label_sprint_cards: Exportar cartões
+ label_sprint_impediments: Impedimentos do Sprint
+ label_sprint_name: 'Sprint "%{name}"'
+ label_sprint_velocity: 'Velocidade %{velocity}, baseado em %{sprints} sprints, com uma média de %{days} dias'
+ label_stories: Histórias
+ label_stories_tasks: Histórias/tarefas
+ label_task_board: Quadro de tarefas
+ label_version_setting: Versões
+ label_webcal: Webcal Feed
+ label_wiki: Wiki
+ permission_view_master_backlog: Visualizar backlog principal
+ permission_view_taskboards: Visualizar quadro de tarefas
+ permission_update_sprints: Editar sprints
+ permission_create_stories: Criar histórias
+ permission_update_stories: Editar histórias
+ permission_create_tasks: Criar tarefas
+ permission_update_tasks: Editar tarefas
+ permission_create_impediments: Criar impedimentos
+ permission_update_impediments: Editar impedimentos
+ points_accepted: pontos aceitos
+ points_committed: pontos comprometidos
+ points_resolved: pontos resolvidos
+ points_to_accept: pontos não aceitos
+ points_to_resolve: pontos não resolvidos
+ project_module_backlogs: Backlogs
+ rb_label_copy_tasks: Copiar pacotes de trabalho
+ rb_label_copy_tasks_all: Todos
+ rb_label_copy_tasks_none: Nenhum
+ rb_label_copy_tasks_open: Aberto
+ rb_label_link_to_original: Incluir link para a história original
+ remaining_hours: horas restantes
+ required_burn_rate_hours: burn rate necessário (horas)
+ required_burn_rate_points: burn rate necessário (pontos)
+ todo_work_package_description: '%{summary}: %{url}%{description}'
+ todo_work_package_summary: '%{type}: %{summary}'
+ version_settings_display_label: Coluna no backlog
+ version_settings_display_option_left: esquerda
+ version_settings_display_option_none: nenhum
+ version_settings_display_option_right: direta
diff --git a/vendored-plugins/openproject-backlogs/config/locales/sv-SE.yml b/vendored-plugins/openproject-backlogs/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..a99e2f074f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/locales/sv-SE.yml
@@ -0,0 +1,144 @@
+sv:
+ activerecord:
+ attributes:
+ work_package:
+ position: Position
+ remaining_hours: Återstående timmar
+ story_points: Berättelsepoäng
+ backlogs_work_package_type: Typ av backlogg
+ errors:
+ models:
+ work_package:
+ attributes:
+ blocks_ids:
+ can_only_contain_work_packages_of_current_sprint: kan endast innehålla ID:n för arbetspaket i den aktuella sprinten.
+ must_block_at_least_one_work_package: måste innehålla ID för minst ett arbetspaket.
+ fixed_version_id:
+ task_version_must_be_the_same_as_story_version: måste vara samma som berättelsens version.
+ parent_id:
+ parent_child_relationship_across_projects: "är ogiltig eftersom arbetspaketet '%{work_package_name}' är en backlog aktivitet och kan därför inte ha en förälder utanför det aktuella projektet."
+ type_must_be_one_of_the_following: 'Typen måste vara en av följande: %{type_names}.'
+ sprint:
+ cannot_end_before_it_starts: En sprint kan inte avslutas innan den startar.
+ backlogs:
+ add_new_story: Ny berättelse
+ any: någon
+ backlog_settings: Inställningar för backlog
+ burndown_graph: Burndown Graph
+ card_paper_size: Pappersstorlek för kortutskrift
+ chart_options: Chart options
+ close: Stäng
+ column_width: 'Kolumnens bredd:'
+ date: Dag
+ definition_of_done: Definition av klart
+ generating_chart: Genererar diagram...
+ hours: Timmar
+ impediment: Hinder
+ label_versions_default_fold_state: Visa ihopfällda versioner
+ work_package_is_closed: Arbetspaket är klart, när
+ label_is_done_status: 'Status %{status_name} innebär klart'
+ no_burndown_data: 'Ingen Burndown data tillgänglig. Start- och slutdatum för sprint måste definieras.'
+ points: Poäng
+ positions_could_not_be_rebuilt: Positioner kunde inte beräknas om.
+ positions_rebuilt_successfully: Positioner beräknades om.
+ properties: Egenskaper
+ rebuild: Rekonstruera
+ rebuild_positions: Rekonstruera positioner
+ remaining_hours: Återstående timmar
+ remaining_hours_ideal: Återstående timmar (ideal)
+ show_burndown_chart: Burndown-diagram
+ story: Berättelse
+ story_points: Berättelsepoäng
+ story_points_ideal: Berättelsepoäng (ideala)
+ task: Aktivitet
+ task_color: Aktivitetsfärg
+ unassigned: Ej tilldelad
+ x_more: '%{count} mer...'
+ backlogs_active: aktiv
+ backlogs_any: någon
+ backlogs_card_specification: Typ av etikett för kortutskrift
+ backlogs_inactive: Projektet visar ingen aktivitet
+ backlogs_points_burn_direction: Burn up/-down poäng
+ backlogs_product_backlog: Produktbacklog
+ backlogs_product_backlog_is_empty: Produktbacklogen är tom
+ backlogs_product_backlog_unsized: Toppen av produktbackloggen har berättelser utan storlek
+ backlogs_sizing_inconsistent: Berättelsernas storlekar avviker ifrån sina uppskattningar
+ backlogs_sprint_notes_missing: Stängda sprinter utan retrospektiv/granskningsanteckningar
+ backlogs_sprint_unestimated: Stängda eller aktiva sprintar med berättelser utan storlek
+ backlogs_sprint_unsized: Projektet har aktiva eller nyligen stängda sprintar med berättelser som saknar storlek
+ backlogs_sprints: Sprinter
+ backlogs_story: Berättelse
+ backlogs_story_type: Berättelsetyp
+ backlogs_task: Aktivitet
+ backlogs_task_type: Aktivitetstyp
+ backlogs_velocity_missing: Ingen hastighet kunde beräknas för detta projekt
+ backlogs_velocity_varies: Hastigheten varierar betydligt över sprinter
+ backlogs_wiki_template: Mall för sprint wiki-sida
+ button_edit_wiki: Redigera wiki-sidor
+ error_intro_plural: 'Följande fel uppstod:'
+ error_intro_singular: 'Följande fel uppstod:'
+ error_outro: Vänligen korrigera ovanstående fel innan du skickar in igen.
+ event_sprint_description: |
+ %{summary}: %{url}
+ %{description}
+ event_sprint_summary: '%{project}: %{summary}'
+ ideal: ideala
+ inclusion: ingår inte i listan
+ label_back_to_project: Tillbaka till projektsidan
+ label_backlog: Backlog
+ label_backlogs: Backloggar
+ label_backlogs_unconfigured: 'Du har inte konfigurerat backloggar ännu. Gå till %{administration} > %{plugins}, klicka på %{configure} länken för denna plugin. När du har ställt in fälten, kan du komma tillbaka till denna sida för att börja använda verktyget.'
+ label_blocks_ids: ID:n för blockerade arbetspaket
+ label_burndown: Burndown
+ label_column_in_backlog: Kolumn i backlog
+ label_hours: timmar
+ label_work_package_hierarchy: Arbetspaketshierarki
+ label_master_backlog: Master Backlog
+ label_not_prioritized: inte prioriterad
+ label_points: poäng
+ label_points_burn_down: Ner
+ label_points_burn_up: Upp
+ label_product_backlog: produkt backlog
+ label_select_all: Markera alla
+ label_sprint_backlog: sprint backlogg
+ label_sprint_cards: Exportera kort
+ label_sprint_impediments: Sprint hinder
+ label_sprint_name: 'Sprint "%{name}"'
+ label_sprint_velocity: 'Hastighet %{velocity}, baserat på %{sprints} sprinter med genomsnitt av %{days} dagar'
+ label_stories: Berättelser
+ label_stories_tasks: Berättelser/uppgifter
+ label_task_board: Aktivitetstavla
+ label_version_setting: Versioner
+ label_webcal: Webcal Feed
+ label_wiki: Wiki
+ permission_view_master_backlog: Visa master backlog
+ permission_view_taskboards: Visa aktivitetstavlor
+ permission_update_sprints: Uppdatera sprinter
+ permission_create_stories: Skapa berättelser
+ permission_update_stories: Uppdatera berättelser
+ permission_create_tasks: Skapa aktiviteter
+ permission_update_tasks: Uppdatera aktiviteter
+ permission_create_impediments: Skapa hinder
+ permission_update_impediments: Uppdatera hinder
+ points_accepted: accepterade poäng
+ points_committed: incheckade poäng
+ points_resolved: lösta poäng
+ points_to_accept: ej accepterade poäng
+ points_to_resolve: ej lösta poäng
+ project_module_backlogs: Backloggar
+ rb_label_copy_tasks: Kopiera arbetspaket
+ rb_label_copy_tasks_all: Alla
+ rb_label_copy_tasks_none: Inga
+ rb_label_copy_tasks_open: Öppna
+ rb_label_link_to_original: Inkludera länk till originalberättelsen
+ remaining_hours: återstående timmar
+ required_burn_rate_hours: tempo som krävs (timmar)
+ required_burn_rate_points: tempo som krävs (poäng)
+ todo_work_package_description: |
+ %{summary}: %{url}
+ %{description}
+ todo_work_package_summary: '%{type}: %{summary}'
+ version_settings_display_label: Kolumn i backlog
+ version_settings_display_option_left: vänster
+ version_settings_display_option_none: inga
+ version_settings_display_option_right: höger
diff --git a/vendored-plugins/openproject-backlogs/config/routes.rb b/vendored-plugins/openproject-backlogs/config/routes.rb
new file mode 100644
index 0000000000..70c1e1eacd
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/config/routes.rb
@@ -0,0 +1,66 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+OpenProject::Application.routes.draw do
+ scope '', as: 'backlogs' do
+ scope 'projects/:project_id', as: 'project' do
+ resources :backlogs, controller: :rb_master_backlogs, only: :index
+
+ resources :sprints, controller: :rb_sprints, only: :update do
+ resource :query, controller: :rb_queries, only: :show
+
+ resource :taskboard, controller: :rb_taskboards, only: :show
+
+ resource :wiki, controller: :rb_wikis, only: [:show, :edit]
+
+ resource :burndown_chart, controller: :rb_burndown_charts, only: :show
+
+ resources :impediments, controller: :rb_impediments, only: [:create, :update]
+
+ resources :tasks, controller: :rb_tasks, only: [:create, :update]
+
+ resources :export_card_configurations, controller: :rb_export_card_configurations, only: [:index, :show] do
+ resources :stories, controller: :rb_stories, only: [:index]
+ end
+
+ resources :stories, controller: :rb_stories, only: [:create, :update]
+ end
+ end
+ end
+
+ get 'projects/:project_id/versions/:id/edit' => 'version_settings#edit'
+ post 'projects/:id/project_done_statuses' => 'projects#project_done_statuses'
+ post 'projects/:id/rebuild_positions' => 'projects#rebuild_positions'
+end
diff --git a/vendored-plugins/openproject-backlogs/db/migrate/20111014073606_aggregated_backlogs_migrations.rb b/vendored-plugins/openproject-backlogs/db/migrate/20111014073606_aggregated_backlogs_migrations.rb
new file mode 100644
index 0000000000..3cea13ee1d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/migrate/20111014073606_aggregated_backlogs_migrations.rb
@@ -0,0 +1,119 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'migration_squasher').to_s
+require Rails.root.join('db', 'migrate', 'migration_utils', 'setting_renamer').to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedBacklogsMigrations < ActiveRecord::Migration
+ def initialize(*)
+ super
+ @issues_table_exists = ActiveRecord::Base.connection.tables.include? 'issues'
+ end
+
+ REPLACED = {
+ 'story_trackers' => 'story_types',
+ 'task_tracker' => 'task_type'
+ }
+
+ MIGRATION_FILES = <<-MIGRATIONS
+ 011_create_stories_tasks_sprints_and_burndown.rb
+ 017_change_issue_position_column.rb
+ 20110321145023_create_version_setting.rb
+ 20110513130147_remove_sprint_start_date.rb
+ 20110610134000_add_projects_issue_statuses.rb
+ 20111014073605_drop_burndown_days.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = 'chiliproject_backlogs'
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table 'version_settings' do |t|
+ t.integer 'project_id'
+ t.integer 'version_id'
+ t.integer 'display'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ end
+ add_index 'version_settings', ['project_id', 'version_id'], name: 'index_version_settings_on_project_id_and_version_id'
+
+ create_table 'issue_done_statuses_for_project', id: false do |t|
+ t.integer 'project_id'
+ t.integer 'issue_status_id'
+ end
+
+ if @issues_table_exists
+ change_table 'issues' do |t|
+ t.integer 'position'
+ t.integer 'story_points'
+ t.float 'remaining_hours'
+ end
+ end
+ end
+ Migration::SettingRenamer.rename('plugin_backlogs', 'plugin_openproject_backlogs')
+ Migration::SettingRenamer.rename('plugin_redmine_backlogs', 'plugin_openproject_backlogs')
+ # Rename Tracker to Type
+ Setting['plugin_openproject_backlogs'] = replace(Setting['plugin_openproject_backlogs'], REPLACED)
+ end
+
+ def down
+ drop_table 'version_settings'
+ drop_table 'issue_done_statuses_for_project'
+ if @issues_table_exists
+ change_table 'issues' do |_t|
+ remove_column 'position'
+ remove_column 'story_points'
+ remove_column 'remaining_hours'
+ end
+ end
+ Setting['plugin_openproject_backlogs'] = replace(Setting['plugin_openproject_backlogs'], REPLACED.invert)
+ end
+
+ private
+
+ def replace(hash, mapping)
+ Hash[hash.map { |k, v| [mapping[k] || k, v] }]
+ end
+
+ def settings_table
+ @settings_table ||= ActiveRecord::Base.connection.quote_table_name('settings')
+ end
+
+ def quote_value(s)
+ ActiveRecord::Base.connection.quote(s)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/migrate/20130625094113_add_backlogs_column_to_work_package.rb b/vendored-plugins/openproject-backlogs/db/migrate/20130625094113_add_backlogs_column_to_work_package.rb
new file mode 100644
index 0000000000..d689bfa2c2
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/migrate/20130625094113_add_backlogs_column_to_work_package.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class AddBacklogsColumnToWorkPackage < ActiveRecord::Migration
+ def change
+ add_column :work_packages, :position, :integer
+ add_column :work_packages, :story_points, :integer
+ add_column :work_packages, :remaining_hours, :float
+ WorkPackage.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/migrate/20130916094370_legacy_issues_backlogs_data_to_work_packages.rb b/vendored-plugins/openproject-backlogs/db/migrate/20130916094370_legacy_issues_backlogs_data_to_work_packages.rb
new file mode 100644
index 0000000000..1da91742c7
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/migrate/20130916094370_legacy_issues_backlogs_data_to_work_packages.rb
@@ -0,0 +1,71 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'utils').to_s
+
+class LegacyIssuesBacklogsDataToWorkPackages < ActiveRecord::Migration
+ def up
+ if legacy_backlog_data_exists?
+ execute <<-SQL
+ UPDATE work_packages AS W
+ SET position = (SELECT L.position FROM legacy_issues AS L WHERE L.id = W.id),
+ story_points = (SELECT L.story_points FROM legacy_issues AS L WHERE L.id = W.id),
+ remaining_hours = (SELECT L.remaining_hours FROM legacy_issues AS L WHERE L.id = W.id)
+ SQL
+ end
+ end
+
+ def down
+ if legacy_backlog_data_exists?
+ execute <<-SQL
+ UPDATE legacy_issues AS L
+ SET position = (SELECT W.position FROM work_packages AS W WHERE W.id = L.id),
+ story_points = (SELECT W.story_points FROM work_packages AS W WHERE W.id = L.id),
+ remaining_hours = (SELECT W.remaining_hours FROM work_packages AS W WHERE W.id = L.id)
+ SQL
+ end
+ end
+
+ private
+
+ class LegacyIssue < ActiveRecord::Base
+ end
+
+ def legacy_backlog_data_exists?
+ backlogs_columns = ['position', 'story_points', 'remaining_hours']
+
+ ActiveRecord::Base.connection.table_exists?('legacy_issues') && (LegacyIssue.column_names & backlogs_columns) == backlogs_columns
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/migrate/20130919092624_add_backlog_columns_to_work_package_journal.rb b/vendored-plugins/openproject-backlogs/db/migrate/20130919092624_add_backlog_columns_to_work_package_journal.rb
new file mode 100644
index 0000000000..991b65fd5b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/migrate/20130919092624_add_backlog_columns_to_work_package_journal.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class AddBacklogColumnsToWorkPackageJournal < ActiveRecord::Migration
+ def change
+ add_column :work_package_journals, :story_points, :integer
+ add_column :work_package_journals, :remaining_hours, :float
+
+ add_index :work_package_journals, [:fixed_version_id,
+ :status_id,
+ :project_id,
+ :type_id],
+ name: 'work_package_journal_on_burndown_attributes'
+ Journal::WorkPackageJournal.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/migrate/20131001132542_rename_issue_status_to_status.rb b/vendored-plugins/openproject-backlogs/db/migrate/20131001132542_rename_issue_status_to_status.rb
new file mode 100644
index 0000000000..3dcfb50aea
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/migrate/20131001132542_rename_issue_status_to_status.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class RenameIssueStatusToStatus < ActiveRecord::Migration
+ def change
+ rename_table :issue_done_statuses_for_project, :done_statuses_for_project
+ rename_column :done_statuses_for_project, :issue_status_id, :status_id
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/seeds.rb b/vendored-plugins/openproject-backlogs/db/seeds.rb
new file mode 100644
index 0000000000..282852e981
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/seeds.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# loads environment-specific seeds. The assumed directory structure in db/ is like this:
+# |___seeds
+# | |___all.rb
+# | |___development.rb
+# | |___test.rb
+# | |___production.rb
+# |___seeds.rb
+
+['all', Rails.env].each do |seed|
+ seed_file = "#{File.dirname(__FILE__)}/seeds/#{seed}.rb"
+ if File.exists?(seed_file)
+ puts "**** Loading #{seed} seed data"
+ require seed_file
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/db/seeds/development.rb b/vendored-plugins/openproject-backlogs/db/seeds/development.rb
new file mode 100644
index 0000000000..697acdc966
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/seeds/development.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+%w{ settings }.each do |file|
+ puts "***** #{file}"
+
+ require "#{File.dirname(__FILE__)}/#{file}"
+end
diff --git a/vendored-plugins/openproject-backlogs/db/seeds/production.rb b/vendored-plugins/openproject-backlogs/db/seeds/production.rb
new file mode 100644
index 0000000000..697acdc966
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/seeds/production.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+%w{ settings }.each do |file|
+ puts "***** #{file}"
+
+ require "#{File.dirname(__FILE__)}/#{file}"
+end
diff --git a/vendored-plugins/openproject-backlogs/db/seeds/settings.rb b/vendored-plugins/openproject-backlogs/db/seeds/settings.rb
new file mode 100644
index 0000000000..dc82c21eeb
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/db/seeds/settings.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+if Setting.where(name: 'plugin_openproject_backlogs').any?
+ puts '****** Skipping settings as there are already some configured'
+else
+ story_types = [Type.where(name: I18n.t(:default_type_epic)).pluck(:id).first.to_s,
+ Type.where(name: I18n.t(:default_type_user_story)).pluck(:id).first.to_s,
+ Type.where(name: I18n.t(:default_type_feature)).pluck(:id).first.to_s,
+ Type.where(name: I18n.t(:default_type_bug)).pluck(:id).first.to_s]
+
+ task_type = Type.where(name: I18n.t(:default_type_task)).pluck(:id).first.to_s
+
+ Setting[:plugin_openproject_backlogs] = {
+ points_burn_direction: 'up',
+ wiki_template: '',
+ story_types: story_types,
+ task_type: task_type
+ }.with_indifferent_access
+end
diff --git a/vendored-plugins/openproject-backlogs/doc/CHANGELOG.md b/vendored-plugins/openproject-backlogs/doc/CHANGELOG.md
new file mode 100644
index 0000000000..51fae02002
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/doc/CHANGELOG.md
@@ -0,0 +1,287 @@
+
+
+# Changelog
+
+* `#10848` Duplicated status select on type switch
+
+## 3.0.7
+
+* new version scheme
+* `#7782` Long target version names destroy layout
+* `#10228` Font size in backlog and taskboard to small
+* `#10232` Work package filter introduced by plugins not displayed
+
+## 3.0.7
+
+* `#8413` Collapse/Expand arrow not displayed
+
+## 3.0.6
+
+* `#5357` Adapt released plugins to base on plugins functionality
+* Migration fix: Also used plugin_redmine_backlogs as old setting name when migrating
+
+## 3.0.5
+
+* `#542` Fix: Deleting the sprint dates still allows to create burndown charts that wont show
+* `#2526` Fix: Burndown chart for future versions not working
+* `#3440` New workpackage form layout
+* `#4118` [FIX] Add missing labels
+* `#4152` Page not found when only one export card configuration is specified
+* `#4223` Fix: Taskboard is translated to "Aufgabenliste" in German
+* `#4224` "Create stories" is translated to "Sprints anlegen" in Roles & Permissions
+* `#4821` Edit sprint from menu
+* `#4862` Fix: Internal error with custom field for projects
+* show description on pdf export modal
+
+## 3.0.5.pre3
+
+* `#2259` [Accessibility] linearisation of issue show form (2)
+* `#3546` Better icon for Timelines Module
+* use pdf export plugin to export story cards
+
+## 3.0.5.pre2
+
+* `#2167` [Accessibility] Link form elements to their label - backlogs - new task
+* `#2229` [Accessibility] low contrast in backlogs task view
+* `#2258` [Accessibility] linearisation of issue show form
+* `#2607` [Agile] Display Storypoints in Taskboard view
+* `#3532` Fix: [API] It is possible to set statuses that are not allowed by the workflow
+* `#3649` A backlog item should get the highest position when moved to another backlog
+
+## 3.0.5.pre1
+
+* `#2395` [Work Package Tracking] Internal Error when entering a character in a number field in a filter
+* `#3339` [Agile] Display work package numbers with 7 digits in backlog view
+* Adaptations for new icon font
+* Fix: Backlog data existence check
+
+## 3.0.4
+
+* `#2728` Prepared public release
+* updated dependency to OpenProject core
+* list story_points in the api/v1 if we have them
+* added icon for new project menu
+
+## 3.0.4.pre10
+
+* Fixed missing migration of setting values
+
+## 3.0.4.pre9
+
+* `#2545` Migrated old plugin settings
+* `#2461` Fixed saving of setting for version fold status
+* `#2413` Squashed old migrations
+
+## 3.0.4.pre8
+
+* `#2274` Add foldable versions
+
+## 3.0.4.pre7
+
+* `#1093` Fix missing permission translations
+* `#2070` Adapted to changed core asset locations
+* fixed specs and cukes
+* fixed taskboard javascript bug
+* Rename Issue to WorkPackage
+
+## 3.0.4.pre6
+
+* `#2039` Adapt to OpenProject core changes
+** Renamed Issue 2 WorkPackage
+** Renamed IssueStatus 2 Status
+** Renamed Tracker 2 Type
+** Adaptions for group assigned work packages
+** Adaptions for new journal handling
+
+## 3.0.4.pre5 - 2013-07-11
+
+* Adapt to OpenProject core changes
+
+## 3.0.4.pre4 - 2013-06-21
+
+* Adapt to OpenProject core changes
+
+## 3.0.4.pre3 - 2013-06-21
+
+### Minor Change
+
+* Use final plugin name schema
+
+## 3.0.4.pre2 - 2013-06-14
+
+### Minor Change
+
+* added dependency to OpenProject core >= 3.0.0beta1
+* more robust tests by dropping IE7 workaround
+
+## 3.0.4.beta - 2013-05-31
+
+### Minor Change
+
+* Fixed Translation bugs
+* Fixed task color setting
+* Fixed sprint date validation
+
+## 3.0.3.rc1 - 2013-05-24
+
+### Major Change
+
+* RC1 of the Rails 3 version
+* This version is no longer compatible with the Rails 2 core
+
+## 2.1.0 - 2012-04-13
+
+### Major Change
+
+* Fixing mass assignment vulnerabilities
+
+## 2.0.4 - 2012-04-03
+
+### Minor Change
+
+* fix acts_as_journalized issues
+* fix showing multiple status in backlogs view
+
+## 2.0.3 - 2012-03-14
+
+### Minor Change
+
+* Fixing typo
+
+
+## 2.0.2 - 2012-03-12
+
+### Minor Change
+
+* Design fixes
+
+## 2.0.1 - 2012-03-02
+
+### Minor Change
+
+* Design fixes
+
+## 2.0.0 - 2012-02-16
+
+Incompatible with older versions of ChiliProject
+
+### Major Change
+
+* Adds support for breadcrumb navigation within OpenProject
+
+## 1.2.7 - 2012-02-13
+
+### Minor Change
+
+* Design fix
+
+## 1.2.6 - 2012-02-02
+
+### Minor Change
+
+* Supporting changes in accessibility master
+** Sub issues are now rendered differently and therefor the link has changed
+
+## 1.2.5 - 2012-01-30
+
+### Minor Change
+
+* Removing BETA
+* Removing font-size definitions to let the global ones kick in
+
+## 1.2.4 - 2012-01-25
+
+### Minor Change
+
+* Improved accessibility of issue box's issue hierarchy view
+
+## 1.2.3 - 2012-01-20
+
+### Minor Test Change
+
+* Moving cucumber step definitions to chiliproject cucumber to use them in other plugins as well.
+
+## 1.2.2 - 2012-01-16
+
+### Minor Test Change
+
+* Moving cucumber step definitions to chiliproject cucumber to use them in other plugins as well.
+
+## 1.2.1 - 2012-01-05
+
+### Minor Change
+
+* Adding missing space on version setting page to fix test and design
+
+## 1.2.0 - 2012-01-02
+
+### Minor Changes
+
+* Improved Ruby 1.9 support
+* Improved support for the proposed fix of Chili #780
+* Moved from symbol keys to string keys for plugin settings
+
+## 1.1.0 - 2011-12-16
+
+### Minor Changes
+
+* Tested with the new layout of yet to be released ChiliProject 3.0
+* Loading jQuery only if ChiliProject doesn't
+
+### Bug Fixes
+
+* Fixing syntactic ambiguity in Ruby 1.9 (#1)
+
+## 1.0.2 - 2011-11-21
+
+### Minor Changes
+
+* Removing unneccesary migrations
+* Added information about the needed core patch
+
+### Bug fixes
+
+* Fixing tests on Postgres
+
+## 1.0.1 - 2011-11-16
+
+### Minor Changes
+
+* Adding links to ChiliProject Plugin configuration.
+* Requiring released version od ChiliProject NIssue Plugin
+
+## 1.0.0 - 2011-11-16 - Initial Release
diff --git a/vendored-plugins/openproject-backlogs/doc/COPYRIGHT.md b/vendored-plugins/openproject-backlogs/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..5523b0546b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/doc/COPYRIGHT.md
@@ -0,0 +1,51 @@
+OpenProject Backlogs Plugin
+
+This Plugin adds features, that enable agile teams to work efficiently with
+OpenProject in Scrum projects.
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+--
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs, whose Copyright follows.
+
+ChiliProject Backlogs Plugin
+
+This Plugin adds features, that enable agile teams to work efficiently with
+ChiliProject in Scrum projects.
+
+
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-backlogs/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-backlogs/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..0d2309ebd3
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/doc/COPYRIGHT_short.md
@@ -0,0 +1,32 @@
+OpenProject Backlogs Plugin
+
+Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+Copyright (C)2010-2011 friflaj
+Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+Copyright (C)2009-2010 Mark Maglana
+Copyright (C)2009 Joe Heck, Nate Lowrie
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+The copyright follows:
+Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
diff --git a/vendored-plugins/openproject-backlogs/doc/GPL.txt b/vendored-plugins/openproject-backlogs/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-backlogs/features/backlog_settings.feature b/vendored-plugins/openproject-backlogs/features/backlog_settings.feature
new file mode 100644
index 0000000000..3f26375fa3
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/backlog_settings.feature
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Backlog Settings
+ As a Project Admin
+ I want to configure the backlogs plugin
+ So that my team and I can work effectively
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the backlogs module is initialized
+ And there is 1 user with:
+ | login | padme |
+ And there is a role "project admin"
+ And the role "project admin" may have the following rights:
+ | edit_project |
+ | manage_project_activities |
+ And the user "padme" is a "project admin"
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And I am already logged in as "padme"
+
+ @javascript
+ Scenario: One can select which status indicate that an work_package is done
+ Given there is 1 project with:
+ | name | parent |
+ And I am working in project "parent"
+ And the project uses the following modules:
+ | backlogs |
+ And I am working in project "ecookbook"
+ When I go to the settings/backlogs_settings page of the project called "ecookbook"
+ Then there should be a "Resolved" field
+ When I check "Resolved"
+ And I press "Save" within "#tab-content-backlogs_settings"
+ When I go to the settings/backlogs_settings page of the project called "ecookbook"
+ Then the "Resolved" checkbox should be checked
diff --git a/vendored-plugins/openproject-backlogs/features/common.feature b/vendored-plugins/openproject-backlogs/features/common.feature
new file mode 100644
index 0000000000..418f1b250c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/common.feature
@@ -0,0 +1,81 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Common
+ As a user
+ I want to do stuff
+ So that I can do my job
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the backlogs module is initialized
+ And there is 1 user with:
+ | login | paul |
+ And there is a role "team member"
+ And the role "team member" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | create_tasks |
+ | update_tasks |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And the user "paul" is a "team member"
+ And I am already logged in as "paul"
+
+ Scenario: View the master backlog
+ Given I am on the overview page of the project
+ And I follow "Backlogs"
+ Then I should be on the master backlog
+
+ Scenario: View the product backlog
+ When I go to the master backlog
+ Then the request should complete successfully
+
+ Scenario: View the product backlog without any stories
+ Given there are no stories in the project
+ When I go to the master backlog
+ Then the request should complete successfully
diff --git a/vendored-plugins/openproject-backlogs/features/edit_story.feature b/vendored-plugins/openproject-backlogs/features/edit_story.feature
new file mode 100644
index 0000000000..f8326ee5f4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/edit_story.feature
@@ -0,0 +1,163 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Edit story on backlogs view
+ As a team member
+ I want to manage story details and story priority on the scrum backlogs view
+ So that I do not loose context while filling in details
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the following types are configured to track stories:
+ | Story |
+ | Epic |
+ | Bug |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Bug |
+ | Task |
+ And there is 1 user with:
+ | login | mathias |
+ And there is a role "team member"
+ And the role "team member" may have the following rights:
+ | view_master_backlog |
+ | create_stories |
+ | update_stories |
+ | view_work_packages |
+ | edit_work_packages |
+ | add_work_packages |
+ | manage_subtasks |
+ And the user "mathias" is a "team member"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2010-03-01 | 2010-03-31 |
+ And the project has the following owner backlogs:
+ | Product Backlog |
+ | Wishlist |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And the type "Story" has the default workflow for the role "team member"
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the project has the following stories in the following owner backlogs:
+ | subject | backlog |
+ | Story 1 | Product Backlog |
+ | Story 2 | Product Backlog |
+ | Story 3 | Product Backlog |
+ | Story 4 | Product Backlog |
+ And the project has the following stories in the following sprints:
+ | subject | sprint | story_points |
+ | Story A | Sprint 001 | 10 |
+ | Story B | Sprint 001 | 20 |
+ And I am already logged in as "mathias"
+
+ @javascript
+ Scenario: Create a new story in the backlog
+ Given I am on the master backlog
+ When I open the "Product Backlog" backlogs menu
+ And I follow "New Story" of the "Product Backlog" backlogs menu
+ And I close the "Product Backlog" backlogs menu
+ And I fill in "Alice in Wonderland" for "subject"
+ And I confirm the story form
+ Then the 1st story in the "Product Backlog" should be "Alice in Wonderland"
+ And the 1st story in the "Product Backlog" should have the ID of "Alice in Wonderland"
+ And I should see 5 stories in "Product Backlog"
+
+ @javascript
+ Scenario: Create a new story in a sprint
+ Given I am on the master backlog
+ When I open the "Sprint 001" backlogs menu
+ And I follow "New Story" of the "Sprint 001" backlogs menu
+ And I close the "Sprint 001" backlogs menu
+ And I fill in "The Wizard of Oz" for "subject"
+ And I fill in "3" for "story_points"
+ And I confirm the story form
+ Then the 1st story in the "Sprint 001" should be "The Wizard of Oz"
+ And the 1st story in the "Sprint 001" should have the ID of "The Wizard of Oz"
+ And I should see 3 stories in "Sprint 001"
+ And the velocity of "Sprint 001" should be "33"
+
+ @javascript
+ Scenario: Edit story in the backlog
+ Given I am on the master backlog
+ When I click on the text "Story 2"
+ And I fill in "Story 2 revisited" for "subject"
+ And I confirm the story form
+ Then I should see 4 stories in "Product Backlog"
+ And the 2nd story in the "Product Backlog" should be "Story 2 revisited"
+
+ @javascript
+ Scenario: Edit story in a sprint
+ Given I am on the master backlog
+ When I click on the text "Story A"
+ And I fill in "Story A revisited" for "subject"
+ And I fill in "11" for "story_points"
+ And I confirm the story form
+ Then the 1st story in the "Sprint 001" should be "Story A revisited"
+ And I should see 2 stories in "Sprint 001"
+ And the velocity of "Sprint 001" should be "31"
+
+ @javascript
+ Scenario: Setting types of a story
+ Given I am on the master backlog
+ When I open the "Sprint 001" backlogs menu
+ And I follow "New Story" of the "Sprint 001" backlogs menu
+ And I close the "Sprint 001" backlogs menu
+ And I fill in "The Wizard of Oz" for "subject"
+ And I select "Bug" from "type_id"
+ And I confirm the story form
+ Then the 1st story in "Sprint 001" should be "The Wizard of Oz"
+ And the 1st story in "Sprint 001" should be in the "Bug" type
+
+ @javascript
+ Scenario: Hiding types, that are not active in the project
+ Given I am on the master backlog
+ When I open the "Sprint 001" backlogs menu
+ And I follow "New Story" of the "Sprint 001" backlogs menu
+ And I close the "Sprint 001" backlogs menu
+ Then I should not see "Epic" within_hidden ".type_id.helper"
diff --git a/vendored-plugins/openproject-backlogs/features/edit_story_tracker_and_status.feature b/vendored-plugins/openproject-backlogs/features/edit_story_tracker_and_status.feature
new file mode 100644
index 0000000000..78ffed1f80
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/edit_story_tracker_and_status.feature
@@ -0,0 +1,157 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Edit story type and status
+ As a user
+ I want to edit the type and the status of a story
+ In consideration of existing workflows
+ So that I can not make changes that are not permitted by the system
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the following types are configured to track stories:
+ | Story |
+ | Epic |
+ | Bug |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Bug |
+ | Task |
+ And there is 1 user with:
+ | login | romano |
+ And there is a role "manager"
+ And the user "romano" is a "manager"
+ And the role "manager" may have the following rights:
+ | view_master_backlog |
+ | create_stories |
+ | update_stories |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the project has the following stories in the following sprints:
+ | subject | sprint | type | status | story_points |
+ | Story A | Sprint 001 | Bug | New | 10 |
+ | Story B | Sprint 001 | Story | New | 20 |
+ | Story C | Sprint 001 | Bug | New | 20 |
+ And the Type "Story" has for the Role "manager" the following workflows:
+ | old_status | new_status |
+ | New | Rejected |
+ | Rejected | Closed |
+ | Rejected | New |
+ And the Type "Bug" has for the Role "manager" the following workflows:
+ | old_status | new_status |
+ | New | Closed |
+ And I am already logged in as "romano"
+ And I am on the master backlog
+
+ @javascript
+ Scenario: Display only statuses which are allowed by workflow
+ When I click on the text "Story A"
+
+ And the available status of the story called "Story A" should be the following:
+ | New |
+ | Closed |
+
+ When I select "Closed" from "status_id"
+ And I confirm the story form
+
+ Then the displayed attributes of the story called "Story A" should be the following:
+ | Status | Closed |
+
+ When I click on the text "Story A"
+
+ Then the available status of the story called "Story A" should be the following:
+ | Closed |
+
+ @javascript
+ Scenario: Select a status and change to a type that does not offer the status
+ When I click on the text "Story B"
+
+ Then the available status of the story called "Story B" should be the following:
+ | New |
+ | Rejected |
+
+ When I select "Rejected" from "status_id"
+ And I select "Bug" from "type_id"
+
+ Then the editable attributes of the story called "Story B" should be the following:
+ | Status | |
+ And the available status of the story called "Story B" should be the following:
+ | New |
+
+ When I confirm the story form
+ Then the error alert should show "Status can't be blank"
+
+ When I press "OK"
+ And I click on the text "Story B"
+ And I select "New" from "status_id"
+ And I confirm the story form
+
+ Then the displayed attributes of the story called "Story B" should be the following:
+ | Status | New |
+
+ @javascript
+ Scenario: Edit a story having no permission for the status of the current ticket
+ When the Type "Bug" has for the Role "manager" the following workflows:
+ | old_status | new_status |
+ | New | Resolved |
+ And I am on the master backlog
+ And I click on the text "Story C"
+
+ Then the available status of the story called "Story C" should be the following:
+ | New |
+ | Resolved |
+
+ When I select "Resolved" from "status_id"
+ And I confirm the story form
+
+ Then the displayed attributes of the story called "Story C" should be the following:
+ | Status | Resolved |
diff --git a/vendored-plugins/openproject-backlogs/features/edit_version_from_menu.feature b/vendored-plugins/openproject-backlogs/features/edit_version_from_menu.feature
new file mode 100644
index 0000000000..324cdb4342
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/edit_version_from_menu.feature
@@ -0,0 +1,78 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Version Settings
+ As a Project Admin
+ I want to configure the backlogs plugin
+ So that my team and I can work effectively
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the backlogs module is initialized
+ And there is 1 user with:
+ | login | padme |
+ And there is a role "project admin"
+ And the role "project admin" may have the following rights:
+ | manage_versions |
+ | view_master_backlog |
+ And the user "padme" is a "project admin"
+ And there is a default status with:
+ | name | new |
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ And I am already logged in as "padme"
+
+ @javascript
+ Scenario: Moving the backlog via the menu
+ Given I am on the master backlog
+
+ When I open the "Sprint 001" backlogs menu
+ When I click on "Properties"
+
+ Then I should be on the edit page of the version "Sprint 001"
+
+ When I select "right" from "Column in backlog"
+ And I submit the form by the "Save" button
+
+ Then I should be on the master backlog
+ And the sprint "Sprint 001" should be displayed to the right
+
+
+
diff --git a/vendored-plugins/openproject-backlogs/features/export_card_configurations.feature b/vendored-plugins/openproject-backlogs/features/export_card_configurations.feature
new file mode 100644
index 0000000000..c4b12029a1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/export_card_configurations.feature
@@ -0,0 +1,103 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Export sprint stories as PDF on the Backlogs view
+ As a team member
+ I want to export stories as a PDF on the scrum backlogs view
+ So that I do not loose context while filling in details
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the following types are configured to track stories:
+ | Story |
+ | Epic |
+ | Bug |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Bug |
+ | Task |
+ And there is 1 user with:
+ | login | mathias |
+ And there is a role "team member"
+ And the role "team member" may have the following rights:
+ | view_master_backlog |
+ | create_stories |
+ | update_stories |
+ | view_work_packages |
+ | edit_work_packages |
+ | add_work_packages |
+ | manage_subtasks |
+ And the user "mathias" is a "team member"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And the type "Story" has the default workflow for the role "team member"
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the project has the following stories in the following sprints:
+ | subject | sprint | story_points |
+ | Story A | Sprint 001 | 10 |
+ | Story B | Sprint 001 | 20 |
+ And I am already logged in as "mathias"
+
+ @javascript
+ Scenario: Export sprint stories as a PDF using the default configuration
+ Given there is the single default export card configuration
+ And I am on the master backlog
+ When I open the "Sprint 001" backlogs menu
+ And I follow "Export" of the "Sprint 001" backlogs menu
+ Then the PDF download dialog should be displayed
+
+ @javascript
+ Scenario: Export sprint stories as a PDF using a selected configuration
+ Given there are multiple export card configurations
+ And I am on the master backlog
+ When I open the "Sprint 001" backlogs menu
+ And I follow "Export" of the "Sprint 001" backlogs menu
+ And I should see a modal window
+ And I click on the link on the modal window with text "Custom 2"
+ Then the PDF download dialog should be displayed
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/features/fixed_version_by_issue_hierarchy.feature b/vendored-plugins/openproject-backlogs/features/fixed_version_by_issue_hierarchy.feature
new file mode 100644
index 0000000000..2c13db11be
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/fixed_version_by_issue_hierarchy.feature
@@ -0,0 +1,138 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: The work_package hierarchy defines the allowed versions for each work_package dependent on the type
+ As a team member
+ I want to CRUD work_packages with a reliable target version system
+ So that I know what target version an work_package can have or will be assigned
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ | identifier | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And there is a role "scrum master"
+ And the role "scrum master" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | update_sprints |
+ | update_stories |
+ | create_impediments |
+ | update_impediments |
+ | update_tasks |
+ | view_wiki_pages |
+ | edit_wiki_pages |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ | create_tasks |
+ | add_work_packages |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the backlogs module is initialized
+ And the following types are configured to track stories:
+ | Story |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Epic |
+ | Task |
+ | Bug |
+ And the type "Task" has the default workflow for the role "scrum master"
+ And there is 1 user with:
+ | login | markus |
+ | firstname | Markus |
+ | Lastname | Master |
+ And the user "markus" is a "scrum master"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2.weeks.ago | 1.week.from_now |
+ | Sprint 005 | 3.weeks.ago | 2.weeks.from_now|
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story A | Sprint 001 |
+ | Story B | Sprint 001 |
+ | Story C | Sprint 002 |
+ And I am already logged in as "markus"
+
+ @javascript
+ Scenario: Creating a task, via the taskboard, as a subtask to a story sets the target version to the story´s version
+ Given I am on the taskboard for "Sprint 001"
+ When I click to add a new task for story "Story A"
+ And I fill in "Task 0815" for "subject"
+ And I press "OK"
+ Then I should see "Task 0815" as a task to story "Story A"
+ And the request on task "Task 0815" is finished
+ And the task "Task 0815" should have "Sprint 001" as its target version
+
+ @javascript
+ Scenario: Stale Object Error when creating task via the taskboard without 'Remaining Hours' after having created a task with 'Remaining Hours' after having created a task without 'Remaining Hours' (bug 9057)
+ Given I am on the taskboard for "Sprint 001"
+ When I click to add a new task for story "Story A"
+ And I fill in "Task1" for "subject"
+ And I fill in "3" for "remaining hours"
+ And I press "OK"
+ And I click to add a new task for story "Story A"
+ And I fill in "Task2" for "subject"
+ And I press "OK"
+ And I click to add a new task for story "Story A"
+ And I fill in "Task3" for "subject"
+ And I fill in "7" for "remaining hours"
+ And I press "OK"
+ And the request on task "Task1" is finished
+ And the request on task "Task2" is finished
+ And the request on task "Task3" is finished
+ Then there should not be a saving error on task "Task3"
+ And the task "Task1" should have "Sprint 001" as its target version
+ And the task "Task2" should have "Sprint 001" as its target version
+ And the task "Task3" should have "Sprint 001" as its target version
+ And task Task1 should have remaining_hours set to 3
+ And task Task3 should have remaining_hours set to 7
+
+ #Scenario: Moving a task between stories on the taskboard
+ # not testable for now
+
diff --git a/vendored-plugins/openproject-backlogs/features/foldable_versions.feature b/vendored-plugins/openproject-backlogs/features/foldable_versions.feature
new file mode 100644
index 0000000000..d3895e6e8b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/foldable_versions.feature
@@ -0,0 +1,110 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Foldable versions in master backlog
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And there is a role "scrum master"
+ And the role "scrum master" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | update_sprints |
+ | update_stories |
+ | view_wiki_pages |
+ | edit_wiki_pages |
+ | view_work_packages |
+ And the backlogs module is initialized
+ And the following types are configured to track stories:
+ | Story |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Task |
+ And there is a default status with:
+ | name | new |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the type "Task" has the default workflow for the role "scrum master"
+ And there is 1 user with:
+ | login | markus |
+ | firstname | Markus |
+ | Lastname | Master |
+ And the user "markus" is a "scrum master"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story A | Sprint 001 |
+ | Story B | Sprint 001 |
+ | Story C | Sprint 001 |
+ And I am already logged in as "markus"
+
+ @javascript
+ Scenario: Version are not folded by default and can be folded
+ When I go to the master backlog
+ Then I should see "Story A" within ".backlog"
+
+ When I change the fold state of a version
+ Then I should not see "Story A" within ".backlog"
+
+ @javascript
+ Scenario: Version fold status depends on users setting
+ When I go to the my settings page
+ And I check "Show versions folded"
+ And I submit the form by the "Save" button
+ Then I should see "Account was successfully updated"
+
+ When I go to the master backlog
+ Then I should not see "Story A" within ".backlog"
+
+ When I change the fold state of a version
+ Then I should see "Story A" within ".backlog"
+
+ Then I go to the my settings page
+ And I uncheck "Show versions folded"
+ And I submit the form by the "Save" button
+ Then I should see "Account was successfully updated"
+
+ When I go to the master backlog
+ Then I should see "Story A" within ".backlog"
+
+ When I change the fold state of a version
+ Then I should not see "Story A" within ".backlog"
diff --git a/vendored-plugins/openproject-backlogs/features/issue_hierarchy_restriction_project_boundaries.feature b/vendored-plugins/openproject-backlogs/features/issue_hierarchy_restriction_project_boundaries.feature
new file mode 100644
index 0000000000..d06d7b56ff
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/issue_hierarchy_restriction_project_boundaries.feature
@@ -0,0 +1,100 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: The work_package hierarchy between backlogs stories and backlogs tasks can not span project boundaries
+ As a scrum user
+ I want to limit the work_package hierarchy to not span project boundaries between backlogs stories and backlogs tasks
+ So that I can manage stories more securely
+
+ Background:
+ Given there is 1 project with:
+ | name | parent_project |
+ | identifier | parent_project |
+ And I am working in project "parent_project"
+ And the project uses the following modules:
+ | backlogs |
+ And there is a role "scrum master"
+ And the role "scrum master" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | update_sprints |
+ | update_stories |
+ | create_impediments |
+ | update_impediments |
+ | update_tasks |
+ | view_wiki_pages |
+ | edit_wiki_pages |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ | create_tasks |
+ | add_work_packages |
+ | add_work_packages |
+ And the backlogs module is initialized
+ And the following types are configured to track stories:
+ | Story |
+ And the type "Task" is configured to track tasks
+ And the project "parent_project" uses the following types:
+ | Story |
+ | Epic |
+ | Task |
+ | Bug |
+ And the type "Task" has the default workflow for the role "scrum master"
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And there is 1 user with:
+ | login | markus |
+ | firstname | Markus |
+ | Lastname | Master |
+ And there is 1 project with:
+ | name | child_project |
+ And the project "child_project" uses the following modules:
+ | backlogs |
+ And the project "child_project" uses the following types:
+ | Story |
+ | Epic |
+ | Task |
+ | Bug |
+ And the user "markus" is a "scrum master" in the project "parent_project"
+ And the user "markus" is a "scrum master" in the project "child_project"
+ And the "cross_project_work_package_relations" setting is set to true
+ And I am already logged in as "markus"
diff --git a/vendored-plugins/openproject-backlogs/features/plugin_administration.feature b/vendored-plugins/openproject-backlogs/features/plugin_administration.feature
new file mode 100644
index 0000000000..421acf9d12
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/plugin_administration.feature
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Plugin Administration
+ As an Admin
+ I want to administer the plugin
+ So that it can be adjusted to the user_specific needs
+
+ Scenario: Fields for configuration
+ Given I am already admin
+ When I go to the configuration page of the "openproject_backlogs" plugin
+ Then there should be a "settings_task_type" field
diff --git a/vendored-plugins/openproject-backlogs/features/product_owner.feature b/vendored-plugins/openproject-backlogs/features/product_owner.feature
new file mode 100644
index 0000000000..1de088ce82
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/product_owner.feature
@@ -0,0 +1,156 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Product Owner
+ As a product owner
+ I want to manage story details and story priority
+ So that they get done according to my requirements
+
+ Background:
+ Given the following types are configured to track stories:
+ | Story |
+ | Epic |
+ And the type "Task" is configured to track tasks
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | Closed | true | false |
+
+ And there is a role "product owner"
+ And the role "product owner" may have the following rights:
+ | view_master_backlog |
+ | create_stories |
+ | update_stories |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+
+ And the type "Story" has the default workflow for the role "product owner"
+ And the type "Epic" has the default workflow for the role "product owner"
+ And the type "Task" has the default workflow for the role "product owner"
+
+ And there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the project uses the following modules:
+ | backlogs |
+ And the project uses the following types:
+ | Story |
+ | Epic |
+ | Task |
+
+ And there is 1 user with:
+ | login | mathias |
+ And the user "mathias" is a "product owner"
+
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2010-03-01 | 2010-03-31 |
+ And the project has the following product owner backlogs:
+ | Product Backlog |
+ | Wishlist |
+ And the project has the following stories in the following product owner backlogs:
+ | subject | backlog |
+ | Story 1 | Product Backlog |
+ | Story 2 | Product Backlog |
+ | Story 3 | Product Backlog |
+ | Story 4 | Product Backlog |
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story A | Sprint 001 |
+ | Story B | Sprint 001 |
+ And I am already logged in as "mathias"
+
+ Scenario: View the product backlog
+ When I go to the master backlog
+ Then I should see the product backlog
+ And I should see 4 stories in the "Product Backlog"
+ And I should see 4 sprint backlogs
+ And I should see 2 product owner backlogs
+
+ Scenario: Create a new story
+ When I go to the master backlog
+ And I want to create a story
+ And I set the backlog of the story to Product Backlog
+ And I set the subject of the story to A Whole New Story
+ And I create the story
+ Then the 1st story in the "Product Backlog" should be "A Whole New Story"
+ And all positions should be unique for each version
+
+ Scenario: Update a story
+ Given I am on the master backlog
+ And I want to edit the story with subject Story 3
+ And I set the subject of the story to Relaxdiego was here
+ And I set the type of the story to Epic
+ When I update the story
+ Then the story should have a subject of Relaxdiego was here
+ And the story should have a type of Epic
+ And the story should be at position 3
+
+ Scenario: Close a story
+ Given I am on the master backlog
+ And I want to edit the story with subject Story 4
+ And I set the status of the story to Closed
+ When I update the story
+ Then the status of the story should be set as closed
+
+ Scenario: Move a story to the top
+ Given I am on the master backlog
+ When I move the 3rd story to the 1st position
+ Then the 1st story in the "Product Backlog" should be "Story 3"
+
+ Scenario: Move a story to the bottom
+ Given I am on the master backlog
+ When I move the 2nd story to the last position
+ Then the 4th story in the "Product Backlog" should be "Story 2"
+
+ Scenario: Move a story down
+ Given I am on the master backlog
+ When I move the 2nd story to the 3rd position
+ Then the 2nd story in the "Product Backlog" should be "Story 3"
+ And the 3rd story in the "Product Backlog" should be "Story 2"
+ And the 4th story in the "Product Backlog" should be "Story 4"
+
+ Scenario: Move a story up
+ Given I am on the master backlog
+ When I move the 4th story to the 2nd position
+ Then the 2nd story in the "Product Backlog" should be "Story 4"
+ And the 3rd story in the "Product Backlog" should be "Story 2"
+ And the 4th story in the "Product Backlog" should be "Story 3"
diff --git a/vendored-plugins/openproject-backlogs/features/scrum_master.feature b/vendored-plugins/openproject-backlogs/features/scrum_master.feature
new file mode 100644
index 0000000000..2a3e0ab624
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/scrum_master.feature
@@ -0,0 +1,319 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Scrum Master
+ As a scrum master
+ I want to manage sprints and their stories
+ So that they get done according the product owner´s requirements
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And there is a role "scrum master"
+ And the role "scrum master" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | update_sprints |
+ | update_stories |
+ | create_impediments |
+ | update_impediments |
+ | update_tasks |
+ | view_wiki_pages |
+ | edit_wiki_pages |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ And the backlogs module is initialized
+ And the following types are configured to track stories:
+ | Story |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Epic |
+ | Task |
+ | Bug |
+ And there are the following issue status:
+ | name | is_closed | is_default |
+ | New | false | true |
+ | In Progress | false | false |
+ | Resolved | false | false |
+ | Closed | true | false |
+ | Rejected | true | false |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the type "Task" has the default workflow for the role "scrum master"
+ And there is 1 user with:
+ | login | markus |
+ | firstname | Markus |
+ | Lastname | Master |
+ And the user "markus" is a "scrum master"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2.weeks.ago | 1.week.from_now |
+ | Sprint 005 | 3.weeks.ago | 2.weeks.from_now|
+ And the project has the following product owner backlogs:
+ | Product Backlog |
+ | Wishlist |
+ And the project has the following stories in the following backlogs:
+ | subject | backlog |
+ | Story 1 | Product Backlog |
+ | Story 2 | Product Backlog |
+ | Story 3 | Product Backlog |
+ | Story 4 | Product Backlog |
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story A | Sprint 001 |
+ | Story B | Sprint 001 |
+ | Story C | Sprint 002 |
+ And the project has the following tasks:
+ | subject | sprint | parent |
+ | Task 1 | Sprint 001 | Story A |
+ And the project has the following impediments:
+ | subject | sprint | blocks |
+ | Impediment 1 | Sprint 001 | Story A |
+ And the project has the following work_packages:
+ | subject | sprint | type |
+ | Epic 1 | Sprint 005 | Epic |
+ And the project has the following stories in the following sprints:
+ | subject | sprint | parent |
+ | Story D | Sprint 005 | Epic 1 |
+ | Story E | Sprint 005 | Epic 1 |
+ And the project has the following tasks:
+ | subject | sprint | parent |
+ | Task 10 | Sprint 005 | Story D |
+ | Task 11 | Sprint 005 | Story D |
+ | Subtask 1 | Sprint 005 | Task 10 |
+ | Subtask 2 | Sprint 005 | Task 10 |
+ | Subtask 3 | Sprint 005 | Task 11 |
+ And the project has the following work_packages:
+ | subject | sprint | parent | type |
+ | Subfeature | Sprint 005 | Task 10 | Bug |
+ | Subsubtask | Sprint 005 | Subfeature | Task |
+ And I am already logged in as "markus"
+
+ Scenario: View stories that have a parent ticket
+ Given I am on the master backlog
+ Then I should see 2 stories in "Sprint 005"
+ And I should not see "Epic 1"
+ And I should not see "Task 10"
+ And I should not see "Subtask 1"
+ And I should not see "Subfeature"
+
+ @javascript
+ Scenario: Create an impediment
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the element with class "add_new" within "#impediments"
+ And I fill in "Bad Company" for "subject"
+ And I fill in the ids of the tasks "Task 1" for "blocks_ids"
+ And I select "Markus Master" from "assigned_to_id"
+ And I press "OK"
+ And the impediment "Bad Company" should signal successful saving
+ Then I should see "Bad Company" within "#impediments"
+
+ @javascript
+ Scenario: Create an impediment blocking an work_package of another sprint
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the element with class "add_new" within "#impediments"
+ And I fill in "Bad Company" for "subject"
+ And I fill in the ids of the stories "Story C" for "blocks_ids"
+ And I select "Markus Master" from "assigned_to_id"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages can only contain IDs of work packages in the current sprint."
+
+ @javascript
+ Scenario: Create an impediment blocking a non existent work_package
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the element with class "add_new" within "#impediments"
+ And I fill in "Bad Company" for "subject"
+ And I fill in "0" for "blocks_ids"
+ And I select "Markus Master" from "assigned_to_id"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages can only contain IDs of work packages in the current sprint."
+
+ @javascript
+ Scenario: Create an impediment without specifying what it blocks
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the element with class "add_new" within "#impediments"
+ And I fill in "Bad Company" for "subject"
+ And I fill in "" for "blocks_ids"
+ And I select "Markus Master" from "assigned_to_id"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages must contain the ID of at least one ticket"
+
+ @javascript
+ Scenario: Update an impediment
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the impediment called "Impediment 1"
+ And I fill in "Bad Company" for "subject"
+ And I fill in the ids of the tasks "Task 1" for "blocks_ids"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal successful saving
+
+ @javascript
+ Scenario: Update an impediment to block an work_package of another sprint
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the impediment called "Impediment 1"
+ And I fill in "Bad Company" for "subject"
+ And I fill in the ids of the stories "Story C" for "blocks_ids"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages can only contain IDs of work packages in the current sprint."
+
+ @javascript
+ Scenario: Update an impediment to block a non existent work_package
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the impediment called "Impediment 1"
+ And I fill in "Bad Company" for "subject"
+ And I fill in "0" for "blocks_ids"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages can only contain IDs of work packages in the current sprint."
+
+ @javascript
+ Scenario: Update an impediment to not block anything
+ Given I am on the taskboard for "Sprint 001"
+ When I click on the impediment called "Impediment 1"
+ And I fill in "Bad Company" for "subject"
+ And I fill in "" for "blocks_ids"
+ And I press "OK"
+ Then I should see "Bad Company" within "#impediments"
+ And the impediment "Bad Company" should signal unsuccessful saving
+ And the error alert should show "IDs of blocked work packages must contain the ID of at least one ticket"
+
+ Scenario: Update sprint details
+ Given I am on the master backlog
+ And I want to edit the sprint named Sprint 001
+ And I want to set the name of the sprint to sprint xxx
+ And I want to set the start_date of the sprint to 2010-03-01
+ And I want to set the effective_date of the sprint to 2010-03-20
+ When I update the sprint
+ Then the request should complete successfully
+ And the sprint should be updated accordingly
+
+ Scenario: Update sprint with no name
+ Given I am on the master backlog
+ And I want to edit the sprint named Sprint 001
+ And I want to set the name of the sprint to an empty string
+ When I update the sprint
+ Then the server should return an update error
+
+ Scenario: Move a story from product backlog to sprint backlog
+ Given I am on the master backlog
+ When I move the story named Story 1 up to the 1st position of the sprint named Sprint 001
+ Then the request should complete successfully
+ When I move the story named Story 4 up to the 2nd position of the sprint named Sprint 001
+ And I move the story named Story 2 up to the 1st position of the sprint named Sprint 002
+ And I move the story named Story 4 up to the 1st position of the sprint named Sprint 001
+ Then Story 4 should be in the 1st position of the sprint named Sprint 001
+ And Story 1 should be in the 2nd position of the sprint named Sprint 001
+ And Story 2 should be in the 1st position of the sprint named Sprint 002
+
+ Scenario: Move a story down in a sprint
+ Given I am on the master backlog
+ When I move the story named Story A below Story B
+ Then the request should complete successfully
+ And Story A should be in the 2nd position of the sprint named Sprint 001
+ And Story B should be the higher item of Story A
+
+ Scenario: view the sprint notes
+ Given I have set the content for wiki page Sprint Template to Sprint Template
+ And I have made Sprint Template the template page for sprint notes
+ And I am on the taskboard for "Sprint 001"
+ When I view the sprint notes
+ Then the request should complete successfully
+ Then the wiki page Sprint 001 should contain Sprint Template
+
+ Scenario: edit the sprint notes
+ Given I have set the content for wiki page Sprint Template to Sprint Template
+ And I have made Sprint Template the template page for sprint notes
+ And I am on the taskboard for "Sprint 001"
+ When I edit the sprint notes
+ Then the request should complete successfully
+ Then the wiki page Sprint 001 should contain Sprint Template
+
+ Scenario: View tasks that have subtasks
+ Given I am on the taskboard for "Sprint 005"
+ Then I should see "Task 10" within "#tasks"
+ And I should see "Task 11" within "#tasks"
+ And I should not see "Subtask 1"
+ And I should not see "Subtask 2"
+ And I should not see "Subtask 3"
+ And I should not see "Epic 1"
+ And I should not see "Subfeature"
+ And I should not see "Subsubtask"
+
+ Scenario: Move stories around in the backlog that have a parent ticket
+ Given I am on the master backlog
+ When I move the story named Story D below Story E
+ Then the request should complete successfully
+ And Story D should be in the 2nd position of the sprint named Sprint 005
+ And Story E should be the higher item of Story D
+
+ @javascript
+ Scenario: View epic, stories, tasks, subtasks in the work_package list
+ Given I am on the work_packages index page
+ Then I should see "Epic 1" within "#content"
+ And I should see "Story D" within "#content"
+ And I should see "Story E" within "#content"
+ And I should see "Task 10" within "#content"
+ And I should see "Task 11" within "#content"
+ And I should see "Subtask 1" within "#content"
+ And I should see "Subtask 2" within "#content"
+ And I should see "Subtask 3" within "#content"
+ And I should see "Subfeature" within "#content"
+ And I should see "Subsubtask" within "#content"
+
+ Scenario: Move a task with subtasks around in the taskboard
+ Given I am on the taskboard for "Sprint 005"
+ When I move the task named Task 10 below Task 11
+ Then the request should complete successfully
+ And Task 11 should be the higher task of Task 10
+ And the story named Story D should have 1 task named Task 10
+ And the story named Story D should have 1 task named Task 11
diff --git a/vendored-plugins/openproject-backlogs/features/shared_versions.feature b/vendored-plugins/openproject-backlogs/features/shared_versions.feature
new file mode 100644
index 0000000000..a6758c776c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/shared_versions.feature
@@ -0,0 +1,96 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Shared Versions
+ As a Team Members
+ I want to use versions shared by other projects
+ So that I can distribute work efficiently
+
+ Background:
+ Given there is 1 project with:
+ | name | parent |
+ And the project "parent" has the following sprints:
+ | name | sharing | start_date | effective_date |
+ | ParentSprint | system | 2010-01-01 | 2010-01-31 |
+ And there is 1 project with:
+ | name | child |
+ And the project "child" uses the following modules:
+ | backlogs |
+ And the following types are configured to track stories:
+ | story |
+ | epic |
+ And the type "task" is configured to track tasks
+ And the project "parent" uses the following types:
+ | story |
+ | task |
+ And the project "child" uses the following types:
+ | story |
+ | task |
+ And I am working in project "child"
+ And there is a default status with:
+ | name | new |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And there is 1 user with:
+ | login | padme |
+ And there is a role "project admin"
+ And the role "project admin" may have the following rights:
+ | manage_versions |
+ | view_work_packages |
+ | view_master_backlog |
+ | create_stories |
+ | update_stories |
+ | edit_work_packages |
+ | manage_subtasks |
+ And the user "padme" is a "project admin"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | ChildSprint | 2010-03-01 | 2010-03-31 |
+ And I am already logged in as "padme"
+
+ Scenario: Inherited Sprints are displayed
+ Given I am on the master backlog
+ Then I should see "ParentSprint" within "#sprint_backlogs_container .backlog:first-child .sprint .name"
+
+ Scenario: Only stories of current project are displayed
+ Given the project "parent" has the following stories in the following sprints:
+ | subject | backlog |
+ | ParentStory | ParentSprint |
+ And the project "child" has the following stories in the following sprints:
+ | subject | backlog |
+ | ChildStory | ParentSprint |
+ And I am on the master backlog
+ Then I should see "ChildStory" within ".backlog .story .subject"
+ And I should not see "ParentStory" within ".backlog .story .subject"
diff --git a/vendored-plugins/openproject-backlogs/features/show_story.feature b/vendored-plugins/openproject-backlogs/features/show_story.feature
new file mode 100644
index 0000000000..11a500bb95
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/show_story.feature
@@ -0,0 +1,87 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Show story
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And there is a role "scrum master"
+ And the role "scrum master" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | update_sprints |
+ | update_stories |
+ | create_impediments |
+ | update_impediments |
+ | update_tasks |
+ | view_wiki_pages |
+ | edit_wiki_pages |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ And the backlogs module is initialized
+ And the following types are configured to track stories:
+ | Story |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Task |
+ And there is a default status with:
+ | name | new |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And the type "Task" has the default workflow for the role "scrum master"
+ And there is 1 user with:
+ | login | markus |
+ | firstname | Markus |
+ | Lastname | Master |
+ And the user "markus" is a "scrum master"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story A | Sprint 001 |
+ And I am already logged in as "markus"
+
+ @javascript
+ Scenario: Show work_package in modal box via click
+ When I go to the master backlog
+ And I click on the link for the story "Story A"
+ Then I should see "Story A" within "#content"
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/_given_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/_given_steps.rb
new file mode 100644
index 0000000000..94e3de39a4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/_given_steps.rb
@@ -0,0 +1,351 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Given /^I am logged out$/ do
+ logout
+end
+
+Given /^I set the (.+) of the story to (.+)$/ do |attribute, value|
+ if attribute == 'type'
+ attribute = 'type_id'
+ value = Type.find_by(name: value).id
+ elsif attribute == 'status'
+ attribute = 'status_id'
+ value = Status.find_by(name: value).id
+ elsif %w[backlog sprint].include? attribute
+ attribute = 'fixed_version_id'
+ value = Version.find_by(name: value).id
+ end
+ @story_params[attribute] = value
+end
+
+Given /^I set the (.+) of the task to (.+)$/ do |attribute, value|
+ value = '' if value == 'an empty string'
+ @task_params[attribute] = value
+end
+
+Given /^I want to create a story(?: in [pP]roject "(.+?)")?$/ do |project_name|
+ project = get_project(project_name)
+ @story_params = initialize_story_params(project)
+end
+
+Given /^I want to create a task for (.+)(?: in [pP]roject "(.+?)")?$/ do |story_subject, project_name|
+ project = get_project(project_name)
+
+ story = Story.find_by(subject: story_subject)
+ @task_params = initialize_task_params(project, story)
+end
+
+Given /^I want to create an impediment for (.+?)(?: in [pP]roject "(.+?)")?$/ do |sprint_subject, project_name|
+ project = get_project(project_name)
+ sprint = Sprint.find_by(name: sprint_subject)
+ @impediment_params = initialize_impediment_params(project, sprint.id)
+end
+
+Given /^I want to edit the task named (.+)$/ do |task_subject|
+ task = Task.find_by(subject: task_subject)
+ task.should_not be_nil
+ @task_params = HashWithIndifferentAccess.new(task.attributes)
+end
+
+Given /^I want to edit the impediment named (.+)$/ do |impediment_subject|
+ impediment = Task.find_by(subject: impediment_subject)
+ impediment.should_not be_nil
+ @impediment_params = HashWithIndifferentAccess.new(impediment.attributes)
+end
+
+Given /^I want to edit the sprint named (.+)$/ do |name|
+ sprint = Sprint.find_by(name: name)
+ sprint.should_not be_nil
+ @sprint_params = HashWithIndifferentAccess.new(sprint.attributes)
+end
+
+Given /^I want to indicate that the impediment blocks (.+)$/ do |blocks_csv|
+ blocks_csv = Story.where(subject: blocks_csv.split(', ')).pluck(:id).join(',')
+ @impediment_params[:blocks] = blocks_csv
+end
+
+Given /^I want to set the (.+) of the sprint to (.+)$/ do |attribute, value|
+ value = '' if value == 'an empty string'
+ @sprint_params[attribute] = value
+end
+
+Given /^I want to set the (.+) of the impediment to (.+)$/ do |attribute, value|
+ value = '' if value == 'an empty string'
+ @impediment_params[attribute] = value
+end
+
+Given /^I want to edit the story with subject (.+)$/ do |subject|
+ @story = Story.find_by(subject: subject)
+ @story.should_not be_nil
+ @story_params = HashWithIndifferentAccess.new(@story.attributes)
+end
+
+Given /^the backlogs module is initialized(?: in [pP]roject "(.*)")?$/ do |project_name|
+ project = get_project(project_name)
+
+ step 'the following types are configured to track stories:', Cucumber::Ast::Table.new([['Story'], ['Epic']])
+ step 'the type "Task" is configured to track tasks'
+ step "the project \"#{project.name}\" uses the following types:", Cucumber::Ast::Table.new([['Story', 'Task']])
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following sprints:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ table.hashes.each do |version|
+ ['effective_date', 'start_date'].each do |date_attr|
+ version[date_attr] = eval(version[date_attr]).strftime('%Y-%m-%d') if version[date_attr].match(/^(\d+)\.(year|month|week|day|hour|minute|second)(s?)\.(ago|from_now)$/)
+ end
+ sprint = Sprint.new version
+ sprint.project = project
+ sprint.save!
+
+ vs = sprint.version_settings.build
+ vs.project = project
+ vs.display_left!
+ vs.save!
+ end
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following (?:product )?(?:owner )?backlogs?:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ table.raw.each do |row|
+ version = Version.create!(project: project, name: row.first)
+
+ vs = version.version_settings.build
+ vs.project = project
+ vs.display_right!
+ vs.save!
+ end
+end
+
+Given /^the [pP]roject(?: "(.+?)")? has the following stories in the following (?:product )?(?:owner )?backlogs:$/ do |project_name, table|
+ if project_name
+ step %{the project "#{project_name}" has the following stories in the following sprints:}, table
+ else
+ step 'the project has the following stories in the following sprints:', table
+ end
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following stories in the following sprints:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ project.work_packages.destroy_all
+ prev_id = ''
+
+ table.hashes.each do |story|
+ params = initialize_story_params(project)
+ params['parent'] = WorkPackage.find_by(subject: story['parent'])
+ params['subject'] = story['subject']
+ params['fixed_version_id'] = Version.find_by(name: story['sprint'] || story['backlog']).id
+ params['story_points'] = story['story_points']
+ params['status_id'] = Status.find_by(name: story['status']).id if story['status']
+ params['type_id'] = Type.find_by(name: story['type']).id if story['type']
+
+ params.delete 'position'
+ # NOTE: We're bypassing the controller here because we're just
+ # setting up the database for the actual tests. The actual tests,
+ # however, should NOT bypass the controller
+ s = Story.create_and_position(params, { project: params[:project], author: params['author'] }, prev_id)
+ prev_id = s.id
+ end
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following tasks:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ User.current = User.first
+
+ as_admin do
+ table.hashes.each do |task|
+ story = Story.find_by(subject: task['parent'])
+ params = initialize_task_params(project, story)
+ params['subject'] = task['subject']
+
+ # NOTE: We're bypassing the controller here because we're just
+ # setting up the database for the actual tests. The actual tests,
+ # however, should NOT bypass the controller
+ atask = Task.create_with_relationships(params, project.id)
+ story.children << atask
+ atask
+ end
+ end
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following work_packages:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ User.current = User.first
+
+ as_admin do
+ table.hashes.each do |task|
+ parent = WorkPackage.find_by(subject: task['parent'])
+ type = Type.find_by(name: task['type'])
+ params = initialize_work_package_params(project, type, parent)
+ params['subject'] = task['subject']
+ version = Version.find_by(name: task['sprint'] || task['backlog'])
+ params['fixed_version_id'] = version.id if version
+
+ # NOTE: We're bypassing the controller here because we're just
+ # setting up the database for the actual tests. The actual tests,
+ # however, should NOT bypass the controller
+ work_package = WorkPackage.new
+ work_package.attributes = params
+ work_package.save!
+ end
+ end
+end
+
+Given /^the [pP]roject(?: "([^\"]*)")? has the following impediments:$/ do |project_name, table|
+ project = get_project(project_name)
+
+ User.current = User.first
+
+ as_admin do
+ table.hashes.each do |impediment|
+ sprint = Sprint.find_by(name: impediment['sprint'])
+ blocks = Story.where(subject: impediment['blocks'].split(', ')).pluck(:id)
+ params = initialize_impediment_params(project, sprint)
+ params['subject'] = impediment['subject']
+ params['blocks_ids'] = blocks.join(',')
+
+ # NOTE: We're bypassing the controller here because we're just
+ # setting up the database for the actual tests. The actual tests,
+ # however, should NOT bypass the controller
+ imp = Impediment.create_with_relationships(params, project.id)
+ imp.blocks_ids += blocks
+ imp.save
+ imp
+ end
+ end
+end
+
+Given /^I have selected card label stock (.+)$/ do |stock|
+ settings = Setting.plugin_openproject_backlogs
+ settings['card_spec'] = stock
+ Setting.plugin_openproject_backlogs = settings
+
+ # If this goes wrong, you are probably missing
+ # openproject_backlogs/config/labels.yml
+ # Run
+ # rake openproject:backlogs:default_labels
+ # to get the ones, shipped with the plugin or
+ # rake openproject:backlogs:current_labels
+ # to get current one, downloaded from the internet.
+end
+
+Given /^I have set my API access key$/ do
+ Setting[:rest_api_enabled] = 1
+ User.current.reload
+ User.current.api_key.should_not be_nil
+ @api_key = User.current.api_key
+end
+
+Given /^I have guessed an API access key$/ do
+ Setting[:rest_api_enabled] = 1
+ @api_key = 'guess'
+end
+
+Given /^I have set the content for wiki page (.+) to (.+)$/ do |title, content|
+ title = Wiki.titleize(title)
+ page = @project.wiki.find_page(title)
+ if !page
+ page = WikiPage.new(wiki: @project.wiki, title: title)
+ page.content = WikiContent.new
+ page.save
+ end
+
+ page.content.text = content
+ page.save.should be_truthy
+end
+
+Given /^I have made (.+) the template page for sprint notes/ do |title|
+ Setting.plugin_openproject_backlogs = Setting.plugin_openproject_backlogs.merge('wiki_template' => Wiki.titleize(title))
+end
+
+Given /^there are no stories in the [pP]roject$/ do
+ @project.work_packages.delete_all
+end
+
+Given /^the type "(.+?)" is configured to track tasks$/ do |type_name|
+ type = Type.find_by(name: type_name)
+ type = FactoryGirl.create(:type, name: type_name) if type.blank?
+
+ Setting.plugin_openproject_backlogs = Setting.plugin_openproject_backlogs.merge('task_type' => type.id)
+end
+
+Given /^the following types are configured to track stories:$/ do |table|
+ story_types = []
+ table.raw.each do |line|
+ name = line.first
+ type = Type.find_by(name: name)
+
+ type = FactoryGirl.create(:type, name: name) if type.blank?
+ story_types << type
+ end
+
+ # otherwise the type id's from the previous test are still active
+ WorkPackage.instance_variable_set(:@backlogs_types, nil)
+
+ Setting.plugin_openproject_backlogs = Setting.plugin_openproject_backlogs.merge('story_types' => story_types.map(&:id))
+end
+
+Given /^the [tT]ype(?: "([^\"]*)")? has for the Role "(.+?)" the following workflows:$/ do |type_name, role_name, table|
+ role = Role.find_by(name: role_name)
+ type = Type.find_by(name: type_name)
+
+ type.workflows = []
+ table.hashes.each do |workflow|
+ old_status = Status.find_by(name: workflow['old_status']).id
+ new_status = Status.find_by(name: workflow['new_status']).id
+ type.workflows.build(old_status_id: old_status, new_status_id: new_status, role: role)
+ end
+ type.save!
+end
+
+Given /^the status of "([^"]*)" is "([^"]*)"$/ do |work_package_subject, status_name|
+ s = WorkPackage.find_by_subject(work_package_subject)
+ s.status = Status.find_by(name: status_name)
+ s.save!
+end
+
+Given /^there is the single default export card configuration$/ do
+ config = ExportCardConfiguration.create!(name: 'Default',
+ per_page: 1,
+ page_size: 'A4',
+ orientation: 'landscape',
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false")
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/_then_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/_then_steps.rb
new file mode 100644
index 0000000000..c8d2c95be4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/_then_steps.rb
@@ -0,0 +1,272 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Then /^(.+) should be in the (\d+)(?:st|nd|rd|th) position of the sprint named (.+)$/ do |story_subject, position, sprint_name|
+ position = position.to_i
+ story = Story.where(subject: story_subject, versions: { name: sprint_name }).joins(:fixed_version).first
+ story_position(story).should == position.to_i
+end
+
+Then /^I should see (\d+) (?:product )?owner backlogs$/ do |count|
+ sprint_backlogs = page.all(:css, '#owner_backlogs_container .sprint')
+ sprint_backlogs.length.should == count.to_i
+end
+
+Then /^I should see (\d+) sprint backlogs$/ do |count|
+ sprint_backlogs = page.all(:css, '#sprint_backlogs_container .sprint')
+ sprint_backlogs.length.should == count.to_i
+end
+
+Then /^I should see the burndown chart for sprint "(.+?)"$/ do |sprint|
+ sprint = Sprint.find_by(name: sprint)
+
+ page.should have_css("#burndown_#{sprint.id}")
+end
+
+Then /^I should see the WorkPackages page$/ do
+ page.should have_css('.workpackages-table')
+end
+
+Then /^I should see the taskboard$/ do
+ page.should have_css('#taskboard')
+end
+
+Then /^I should see the product backlog$/ do
+ page.should have_css('#owner_backlogs_container')
+end
+
+Then /^I should see (\d+) stories in (?:the )?"(.+?)"$/ do |count, backlog_name|
+ sprint = Sprint.find_by(name: backlog_name)
+ page.all(:css, "#backlog_#{sprint.id} .story").size.should == count.to_i
+end
+
+Then /^the velocity of "(.+?)" should be "(.+?)"$/ do |backlog_name, velocity|
+ sprint = Sprint.find_by(name: backlog_name)
+ page.find(:css, "#backlog_#{sprint.id} .velocity").text.should == velocity
+end
+
+Then /^show me the list of sprints$/ do
+ sprints = Sprint.where(project_id: @project.id)
+
+ puts "\n"
+ puts "\t| #{'id'.ljust(3)} | #{'name'.ljust(18)} | #{'start_date'.ljust(18)} | #{'effective_date'.ljust(18)} | #{'updated_on'.ljust(20)}"
+ sprints.each do |sprint|
+ puts "\t| #{sprint.id.to_s.ljust(3)} | #{sprint.name.to_s.ljust(18)} | #{sprint.start_date.to_s.ljust(18)} | #{sprint.effective_date.to_s.ljust(18)} | #{sprint.updated_on.to_s.ljust(20)} |"
+ end
+ puts "\n\n"
+end
+
+Then /^show me the list of stories$/ do
+ stories = Story.where(project_id: @project.id).order('position ASC')
+ subject_max = (stories.map(&:subject) << 'subject').sort { |a, b| a.length <=> b.length }.last.length
+ sprints = @project.versions
+ sprint_max = (sprints.map(&:name) << 'sprint').sort { |a, b| a.length <=> b.length }.last.length
+
+ puts "\n"
+ puts "\t| #{'id'.ljust(5)} | #{'position'.ljust(8)} | #{'status'.ljust(12)} | #{'rank'.ljust(4)} | #{'subject'.ljust(subject_max)} | #{'sprint'.ljust(sprint_max)} |"
+ stories.each do |story|
+ puts "\t| #{story.id.to_s.ljust(5)} | #{story.position.to_s.ljust(8)} | #{story.status.name[0, 12].ljust(12)} | #{story.rank.to_s.ljust(4)} | #{story.subject.ljust(subject_max)} | #{(story.fixed_version_id.nil? ? Sprint.new : Sprint.find(story.fixed_version_id)).name.ljust(sprint_max)} |"
+ end
+ puts "\n\n"
+end
+
+Then /^(.+) should be the higher (story|item|task) of (.+)$/ do |higher_subject, type, lower_subject|
+ work_package_class = (type == 'task') ? Task : Story
+
+ higher = work_package_class.where(subject: higher_subject)
+ higher.length.should == 1
+
+ lower = work_package_class.where(subject: lower_subject)
+ lower.length.should == 1
+
+ if type == 'task'
+ lower.first.id.should == higher.first.right_sibling.id
+ else
+ lower.first.higher_item.id.should == higher.first.id
+ end
+end
+
+Then /^the request should complete successfully$/ do
+ page.driver.response.status.should == 200
+end
+
+Then /^the request should fail$/ do
+ page.driver.response.status.should == 401
+end
+
+Then /^the (\d+)(?:st|nd|rd|th) story in (?:the )?"(.+?)" should be "(.+)"$/ do |position, version_name, subject|
+ version = Version.find_by(name: version_name)
+ story = Story.at_rank(@project.id, version.id, position.to_i)
+ story.should_not be_nil
+ story.subject.should == subject
+end
+
+Then /^the (\d+)(?:st|nd|rd|th) story in (?:the )?"(.+?)" should be in the "(.+?)" type$/ do |position, version_name, type_name|
+ version = Version.find_by(name: version_name)
+ type = Type.find_by(name: type_name)
+ story = Story.at_rank(@project.id, version.id, position.to_i)
+ story.should_not be_nil
+ story.type.should == type
+end
+
+Then /^the (\d+)(?:st|nd|rd|th) story in (?:the )?"(.+?)" should have the ID of "(.+?)"$/ do |position, version_name, subject|
+ version = Version.find_by(name: version_name)
+ actual_story = WorkPackage.find_by_subject_and_fixed_version_id(subject, version)
+ step %%I should see "#{actual_story.id}" within "#backlog_#{version.id} .story:nth-child(#{position}) .id div.t"%
+end
+
+Then /^all positions should be unique for each version$/ do
+ Story.find_by_sql("select project_id, fixed_version_id, position, count(*) as dups from #{WorkPackage.table_name} where not position is NULL group by project_id, fixed_version_id, position having count(*) > 1").length.should == 0
+end
+
+Then /^the (\d+)(?:st|nd|rd|th) task for (.+) should be (.+)$/ do |position, story_subject, task_subject|
+ story = Story.find_by(subject: story_subject)
+ story.children[position.to_i - 1].subject.should == task_subject
+end
+
+Then /^the server should return an update error$/ do
+ page.driver.response.status.should == 400
+end
+
+Then /^the server should return (\d+) updated (.+)$/ do |count, object_type|
+ page.all("##{object_type.pluralize} .#{object_type.singularize}").length.should == count.to_i
+end
+
+Then /^the sprint named (.+) should have (\d+) impediments? named (.+)$/ do |sprint_name, count, impediment_subject|
+ sprints = Sprint.where(name: sprint_name)
+ sprints.length.should == 1
+
+ sprints.first.impediments.map { |i| i.subject == impediment_subject }.length.should == count.to_i
+end
+
+Then /^the impediment "(.+)" should signal( | un)successful saving$/ do |impediment_subject, negative|
+ pos_or_neg_should = !negative.blank? ? :should : :should_not
+
+ page.send(pos_or_neg_should, have_selector('div.impediment.error', text: impediment_subject))
+end
+
+Then /^the sprint should be updated accordingly$/ do
+ sprint = Sprint.find(@sprint_params['id'])
+
+ sprint.attributes.each_key do |key|
+ unless ['updated_on', 'created_on'].include?(key)
+ (key.include?('_date') ? sprint[key].strftime('%Y-%m-%d') : sprint[key]).should == @sprint_params[key]
+ end
+ end
+end
+
+Then /^the status of the story should be set as (.+)$/ do |status|
+ @story.reload
+ @story.status.name.downcase.should == status
+end
+
+Then /^the story named (.+) should have 1 task named (.+)$/ do |story_subject, task_subject|
+ stories = Story.where(subject: story_subject)
+ stories.length.should == 1
+
+ tasks = Task.where(parent_id: stories.first.id, subject: task_subject)
+ tasks.length.should == 1
+end
+
+Then /^the story should be at the (top|bottom)$/ do |position|
+ if position == 'top'
+ story_position(@story).should == 1
+ else
+ story_position(@story).should == @story_ids.length
+ end
+end
+
+Then /^the story should be at position (.+)$/ do |position|
+ story_position(@story).should == position.to_i
+end
+
+Then /^the story should have a (.+) of (.+)$/ do |attribute, value|
+ @story.reload
+ if attribute == 'type'
+ attribute = 'type_id'
+ value = Type.find_by(name: value).id
+ end
+ @story[attribute].should == value
+end
+
+Then /^the wiki page (.+) should contain (.+)$/ do |title, content|
+ title = Wiki.titleize(title)
+ page = @project.wiki.find_page(title)
+ page.should_not be_nil
+
+ raise "\"#{content}\" not found on page \"#{title}\"" unless page.content.text.match(/#{content}/)
+end
+
+Then /^(work_package|task|story) (.+) should have (.+) set to (.+)$/ do |_type, subject, attribute, value|
+ work_package = WorkPackage.find_by_subject(subject)
+ work_package[attribute].should == value.to_i
+end
+
+Then /^the error alert should show "(.+?)"$/ do |msg|
+ step %{I should see "#{msg}" within "#msgBox"}
+end
+
+Then /^the start date of "(.+?)" should be "(.+?)"$/ do |sprint_name, date|
+ version = Version.find_by(name: sprint_name)
+
+ step %{I should see "#{date}" within "div#sprint_#{version.id} div.start_date"}
+end
+
+Then /^I should see "(.+?)" as a task to story "(.+?)"$/ do |task_name, story_name|
+ story = Story.find_by_subject(story_name)
+
+ step %{I should see "#{task_name}" within "tr.story_#{story.id}"}
+end
+
+Then /^the (?:work_package|task|story) "(.+?)" should have "(.+?)" as its target version$/ do |task_name, version_name|
+ work_package = WorkPackage.find_by_subject(task_name)
+ version = Version.find_by(name: version_name)
+
+ work_package.fixed_version.should eql version
+end
+
+Then /^there should not be a saving error on task "(.+?)"$/ do |task_name|
+ elements = all(:xpath, "//*[contains(., \"#{task_name}\")]")
+ task_div = elements.find { |e| e.tag_name == 'div' && e[:class].include?('task') }
+ task_div[:class].should_not include('error')
+end
+
+Then /^I should be notified that the work_package "(.+?)" is an invalid parent to the work_package "(.+?)" because of cross project limitations$/ do |parent_name, child_name|
+ step %{I should see "Parent is invalid because the work package '#{child_name}' is a backlog task and therefore cannot have a parent outside of the current project." within "#errorExplanation"}
+end
+
+Then /^the PDF download dialog should be displayed$/ do
+ # As far as I'm aware there's nothing that can be done here to check for this.
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/_when_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/_when_steps.rb
new file mode 100644
index 0000000000..4a234e592a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/_when_steps.rb
@@ -0,0 +1,252 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+When /^I create the impediment$/ do
+ page.driver.post backlogs_project_sprint_impediments_url(
+ *@impediment_params.values_at('project_id', 'fixed_version_id')
+ ), @impediment_params
+end
+
+When /^I create the story$/ do
+ page.driver.post backlogs_project_sprint_stories_url(
+ *@story_params.values_at('project_id', 'fixed_version_id')
+ ), @story_params
+end
+
+When /^I create the task$/ do
+ page.driver.post backlogs_project_sprint_tasks_url(
+ *@task_params.values_at('project_id', 'fixed_version_id')
+ ), @task_params
+end
+
+When /^I move the (story|item|task) named (.+) below (.+)$/ do |type, story_subject, prev_subject|
+ work_package_class = if type.strip == 'task' then Task else Story end
+ story = work_package_class.find_by(subject: story_subject.strip)
+ prev = work_package_class.find_by(subject: prev_subject.strip)
+
+ attributes = story.attributes
+ attributes[:prev] = prev.id
+ attributes[:fixed_version_id] = prev.fixed_version_id unless type == 'task'
+
+ project = Project.find(attributes['project_id'])
+ sprint = prev.fixed_version
+
+ page.driver.post polymorphic_url(
+ [:backlogs, project, sprint.becomes(Sprint), story]
+ ), attributes.merge('_method' => 'put')
+end
+
+When /^I move the story named (.+) (up|down) to the (\d+)(?:st|nd|rd|th) position of the sprint named (.+)$/ do |story_subject, direction, position, sprint_name|
+ position = position.to_i
+ story = Story.find_by(subject: story_subject)
+ sprint = Sprint.find_by(name: sprint_name)
+ story.fixed_version = sprint
+
+ attributes = story.attributes
+ attributes[:prev] = if position == 1
+ ''
+ else
+ stories = Story.where(fixed_version_id: sprint.id, type_id: Story.types).order('position ASC')
+ raise "You indicated an invalid position (#{position}) in a sprint with #{stories.length} stories" if 0 > position or position > stories.length
+ stories[position - (direction == 'up' ? 2 : 1)].id
+ end
+
+ page.driver.post backlogs_project_sprint_story_url(
+ *attributes.values_at('project_id', 'fixed_version_id', 'id')
+ ), attributes.merge('_method' => 'put')
+end
+
+When /^I move the (\d+)(?:st|nd|rd|th) story to the (\d+|last)(?:st|nd|rd|th)? position$/ do |old_pos, new_pos|
+ @story_ids = page.all(:css, '#owner_backlogs_container .stories .story .id')
+
+ story = @story_ids[old_pos.to_i - 1]
+ story.should_not == nil
+
+ prev = if new_pos.to_i == 1
+ nil
+ elsif new_pos == 'last'
+ @story_ids.last
+ elsif old_pos.to_i > new_pos.to_i
+ @story_ids[new_pos.to_i - 2]
+ else
+ @story_ids[new_pos.to_i - 1]
+ end
+
+ @story = Story.find(story.text.to_i)
+
+ page.driver.post backlogs_project_sprint_story_url(
+ @project.id,
+ @story.fixed_version_id,
+ @story.id
+ ), prev: (prev.nil? ? '' : prev.text), '_method' => 'put'
+end
+
+When /^I update the impediment$/ do
+ page.driver.post backlogs_project_sprint_impediment_url(
+ *@impediment_params.values_at('project_id', 'fixed_version_id', 'id')
+ ), @impediment_params.merge('_method' => 'put')
+end
+
+When /^I update the sprint$/ do
+ page.driver.post backlogs_project_sprint_url(
+ *@sprint_params.values_at('project_id', 'id')
+ ), @sprint_params.merge('_method' => 'put')
+end
+
+When /^I update the story$/ do
+ page.driver.post backlogs_project_sprint_story_url(
+ *@story_params.values_at('project_id', 'fixed_version_id', 'id')
+ ), @story_params.merge('_method' => 'put')
+end
+
+When /^I update the task$/ do
+ page.driver.post backlogs_project_sprint_task_url(
+ *@task_params.values_at('project_id', 'fixed_version_id', 'id')
+ ), @task_params.merge('_method' => 'put')
+end
+
+When /^I view the master backlog$/ do
+ visit url_for(controller: '/projects', action: :show, id: @project)
+ click_link('Backlogs')
+end
+
+When /^I view the stories of (.+) in the work_packages tab/ do |sprint_name|
+ sprint = Sprint.find_by(name: sprint_name)
+ visit url_for(controller: '/rb_queries', action: :show, project_id: sprint.project, sprint_id: sprint, only_path: true)
+end
+
+When /^I view the stories in the work_packages tab/ do
+ visit url_for(controller: '/rb_queries', action: :show, project_id: @project, only_path: true)
+end
+
+# WARN: Depends on deprecated behavior of path_for('the task board for
+# "sprint name"')
+When /^I view the sprint notes$/ do
+ visit url_for(controller: '/rb_wikis', action: 'show', sprint_id: @sprint)
+end
+
+# WARN: Depends on deprecated behavior of path_for('the task board for
+# "sprint name"')
+When /^I edit the sprint notes$/ do
+ visit url_for(controller: '/rb_wikis', action: 'edit', sprint_id: @sprint)
+end
+
+When /^I follow "(.+?)" of the "(.+?)" (?:backlogs )?menu$/ do |link, backlog_name|
+ sprint = Sprint.find_by(name: backlog_name)
+ step %{I follow "#{link}" within "#backlog_#{sprint.id} .menu"}
+end
+
+When /^I open the "(.+?)" backlogs(?: )?menu/ do |backlog_name|
+ sprint = Sprint.find_by(name: backlog_name)
+ step %{I hover over "#backlog_#{sprint.id} .menu"}
+end
+
+When /^I close the "(.+?)" backlogs(?: )?menu/ do |backlog_name|
+ sprint = Sprint.find_by(name: backlog_name)
+ step %{I stop hovering over "#backlog_#{sprint.id} .menu"}
+end
+
+When /^I click on the text "(.+?)"$/ do |locator|
+ find(:xpath, %{//*[contains(text(), "#{locator}")]}).click
+end
+
+When /^I click on the link on the modal window with text "(.+?)"$/ do |locator|
+ browser = page.driver.browser
+ browser.switch_to.frame('modalIframe')
+ click_link(locator)
+end
+
+When /^I click on the element with class "([^"]+?)"$/ do |locator|
+ find(:css, ".#{locator}").click
+end
+
+When /^I confirm the story form$/ do
+ find(:xpath, XPath::HTML.fillable_field('subject')).native.send_key :return
+ step 'I wait for AJAX requests to finish'
+ step 'I should not see ".saving"'
+end
+
+When /^I fill in the ids of the (tasks|work_packages|stories) "(.+?)" for "(.+?)"$/ do |model_name, subjects, field|
+ model = Kernel.const_get(model_name.classify)
+ ids = subjects.split(/,/).map { |subject| model.find_by_subject(subject).id }
+
+ step %{I fill in "#{ids.join(', ')}" for "#{field}"}
+end
+
+When /^I click on the impediment called "(.+?)"$/ do |impediment_name|
+ step %{I click on the text "#{impediment_name}"}
+end
+
+When /^I click to add a new task for story "(.+?)"$/ do |story_name|
+ story = Story.find_by_subject(story_name)
+
+ page.all(:css, "tr.story_#{story.id} td.add_new").last.click
+end
+
+When /^I fill in the id of the work_package "(.+?)" as the parent work_package$/ do |work_package_name|
+ work_package = WorkPackage.find_by_subject(work_package_name)
+
+ # TODO: Simplify once the work_package#edit/update action is implemented
+ find('#work_package_parent_id, #work_package_parent_id', visible: false).set(work_package.id)
+end
+
+When /^the request on task "(.+?)" is finished$/ do |task_name|
+ # Wait for the modal link of this task to appear...
+ elements = page.find(:xpath, "//div[contains(., '#{task_name}') and contains(@class,'task')]/descendant::a[contains(@href, .)]")
+ # ...by selecting the task board card for a specific task and then go for the
+ # link with the id only appearing after the task was saved
+end
+
+When /^I follow the link to add a subtask$/ do
+ step 'I follow "Relations"'
+ step 'I click "Children"'
+ step 'I press "Add child"'
+end
+
+When /^I change the fold state of a version$/ do
+ find('.backlog .toggler').click
+end
+
+When /^I click on the Export link$/ do
+ click_link('Export')
+end
+
+When(/^I click on the link for the story "(.*?)"$/) do |subject|
+ story = Story.find_by(subject: subject)
+
+ within("#story_#{story.id}") do
+ click_link(story.id)
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/helpers.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/helpers.rb
new file mode 100644
index 0000000000..ba52f60f7c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/helpers.rb
@@ -0,0 +1,117 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+def initialize_story_params(project, user = User.first)
+ data = Story.new.attributes.slice(RbStoriesController::PERMITTED_PARAMS)
+ story = HashWithIndifferentAccess.new(data)
+ story['type_id'] = Story.types.first
+
+ # unsafe attributes that will not be used directly but added for your
+ # convenience
+ story['project_id'] = project.id
+ story['author_id'] = user.id
+ story['project'] = project
+ story['author'] = user
+ story
+end
+
+def initialize_task_params(project, story, user = User.first)
+ params = HashWithIndifferentAccess.new
+ params['type_id'] = Task.type
+ if story
+ params['fixed_version_id'] = story.fixed_version_id
+ params['parent_id'] = story.id
+ end
+ params['status_id'] = Status.first.id
+
+ # unsafe attributes that will not be used directly but added for your
+ # convenience
+ params['project_id'] = project.id
+ params['author_id'] = user.id
+ params['project'] = project
+ params['author'] = user
+ params
+end
+
+def initialize_work_package_params(project, type = Type.first, parent = nil, user = User.first)
+ params = HashWithIndifferentAccess.new
+ params['type_id'] = type.id
+ params['parent_id'] = parent.id if parent
+ params['status_id'] = Status.first.id
+
+ # unsafe attributes that will not be used directly but added for your
+ # convenience
+ params['project_id'] = project.id
+ params['author_id'] = user.id
+ params['project'] = project
+ params['author'] = user
+ params
+end
+
+def initialize_impediment_params(project, sprint, user = User.first)
+ params = HashWithIndifferentAccess.new(RbTasksController::PERMITTED_PARAMS)
+ params['type_id'] = Task.type
+ params['fixed_version_id'] = sprint.id
+ params['status_id'] = Status.first.id
+
+ # unsafe attributes that will not be used directly but added for your
+ # convenience
+ params['project_id'] = project.id
+ params['author_id'] = user.id
+ params['project'] = project
+ params['author'] = user
+ params
+end
+
+def task_position(task)
+ p1 = task.story.tasks.select { |t| t.id == task.id }[0].rank
+ p2 = task.rank
+ p1.should == p2
+ p1
+end
+
+def story_position(story)
+ p1 = Story.sprint_backlog(story.project, story.fixed_version).detect { |s| s.id == story.id }.rank
+ p2 = story.rank
+ p1.should == p2
+
+ Story.at_rank(story.project_id, story.fixed_version_id, p1).id.should == story.id
+ p1
+end
+
+def logout
+ visit url_for(controller: '/account', action: 'logout')
+ @user = nil
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/sprint_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/sprint_steps.rb
new file mode 100644
index 0000000000..2f8b1552a0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/sprint_steps.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+Then /^the sprint "(.+?)" should be displayed to the (right|left)$/ do |sprint_name, orientation|
+ selector = case orientation
+ when 'right'
+ '#owner_backlogs_container'
+ when 'left'
+ '#sprint_backlogs_container'
+ else
+ raise 'Only right and left are supported'
+ end
+
+ step %{I should see "#{sprint_name}" within "#{selector} .name"}
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/story_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/story_steps.rb
new file mode 100644
index 0000000000..fb0f88d79e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/story_steps.rb
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Then(/^the available status of the story called "(.+?)" should be the following:$/) do |story_name, table|
+ # the order of the available status is important
+ story = Story.find_by_subject(story_name)
+
+ expected = table.raw.flatten.join(' ')
+
+ within("#story_#{story.id} .editors") do
+ should have_field('status_id', text: expected)
+ end
+end
+
+Then(/^the displayed attributes of the story called "(.+?)" should be the following:$/) do |story_name, table|
+ story = Story.find_by_subject(story_name)
+
+ within("#story_#{story.id}") do
+ table.rows_hash.each do |key, value|
+ case key
+ when 'Status'
+ within('.status_id') do
+ should have_selector('div.t', text: value)
+ end
+ else
+ raise 'Not an implemented attribute'
+ end
+ end
+ end
+end
+
+Then(/^the editable attributes of the story called "(.+?)" should be the following:$/) do |story_name, table|
+ story = Story.find_by_subject(story_name)
+
+ within("#story_#{story.id} .editors") do
+ table.rows_hash.each do |key, value|
+ case key
+ when 'Status'
+ should have_select('status_id', text: value)
+ else
+ raise 'Not an implemented attribute'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/features/step_definitions/version_steps.rb b/vendored-plugins/openproject-backlogs/features/step_definitions/version_steps.rb
new file mode 100644
index 0000000000..4d3b410afa
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/step_definitions/version_steps.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Then(/^the editable attributes of the version should be the following:$/) do |table|
+ table.rows_hash.each do |key, value|
+ case key
+ when 'Column in backlog'
+ page.should have_select(key, selected: value)
+ when 'Start date'
+ page.should have_field(key, with: value)
+ else
+ raise 'Not an implemented attribute'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/features/support/paths.rb b/vendored-plugins/openproject-backlogs/features/support/paths.rb
new file mode 100644
index 0000000000..23814db1ed
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/support/paths.rb
@@ -0,0 +1,79 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module BacklogsNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the master backlog(?: of the [pP]roject "(.+?)")?$/
+ project = get_project($1)
+ "/projects/#{project.identifier}/backlogs"
+
+ when /^the (:?overview ?)?page (?:for|of) the [pP]roject$/
+ project = get_project
+ path_to %{the overview page of the project called "#{project.name}"}
+
+ when /^the work_packages index page$/
+ project = get_project
+ path_to %{the work packages index page of the project called "#{project.name}"}
+
+ when /^the burndown for "(.+?)"(?: (?:in|of) the [pP]roject "(.+?)")?$/
+ project = get_project($2)
+ sprint = Sprint.find_by_name_and_project_id($1, project)
+
+ "/projects/#{project.identifier}/sprints/#{sprint.id}/burndown_chart"
+
+ when /^the task ?board for "(.+?)"(?: (?:in|of) the [pP]roject "(.+?)")?$/
+ project = get_project($2)
+ sprint = Sprint.find_by_name_and_project_id($1, project)
+
+ # WARN: Deprecated side effect to keep some old-style step definitions.
+ # Do not depend on @sprint being set in new step definitions.
+ @sprint = sprint
+
+ "/projects/#{project.identifier}/sprints/#{sprint.id}/taskboard"
+
+ else
+ super
+ end
+ end
+end
+
+World(BacklogsNavigationHelpers)
diff --git a/vendored-plugins/openproject-backlogs/features/team_member.feature b/vendored-plugins/openproject-backlogs/features/team_member.feature
new file mode 100644
index 0000000000..34d010343b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/team_member.feature
@@ -0,0 +1,174 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Team Member
+ As a team member
+ I want to manage update stories and tasks
+ So that I can update everyone on the status of the project
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the following types are configured to track stories:
+ | Story |
+ | Epic |
+ And the type "Task" is configured to track tasks
+ And the project uses the following types:
+ | Story |
+ | Task |
+ And there is a default status with:
+ | name | new |
+ And there is a default issuepriority with:
+ | name | Normal |
+ And there is 1 user with:
+ | login | paul |
+ And there is a role "team member"
+ And the role "team member" may have the following rights:
+ | view_master_backlog |
+ | view_taskboards |
+ | create_tasks |
+ | update_tasks |
+ | view_work_packages |
+ | edit_work_packages |
+ | manage_subtasks |
+ And the user "paul" is a "team member"
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2010-03-01 | 2010-03-31 |
+ And the project has the following stories in the following sprints:
+ | subject | sprint |
+ | Story 1 | Sprint 001 |
+ | Story 2 | Sprint 001 |
+ | Story 3 | Sprint 001 |
+ | Story 4 | Sprint 002 |
+ And the project has the following tasks:
+ | subject | parent |
+ | Task 1 | Story 1 |
+ And the project has the following impediments:
+ | subject | sprint | blocks |
+ | Impediment 1 | Sprint 001 | Story 1 |
+ | Impediment 2 | Sprint 001 | Story 2 |
+ And I am already logged in as "paul"
+
+ Scenario: Create a task for a story
+ Given I am on the taskboard for "Sprint 001"
+ And I want to create a task for Story 1
+ And I set the subject of the task to A Whole New Task
+ When I create the task
+ Then the request should complete successfully
+ And the 1st task for Story 1 should be A Whole New Task
+
+ Scenario: Update a task for a story
+ Given I am on the taskboard for "Sprint 001"
+ And I want to edit the task named Task 1
+ And I set the subject of the task to Whoa there, Sparky
+ When I update the task
+ Then the request should complete successfully
+ And the story named Story 1 should have 1 task named Whoa there, Sparky
+
+ Scenario: View a taskboard
+ Given I am on the taskboard for "Sprint 001"
+ Then I should see the taskboard
+
+ @javascript
+ Scenario: View the burndown chart from the backlogs dashboard
+ Given I am on the master backlog
+ And I open the "Sprint 002" backlogs menu
+ When I follow "Burndown Chart" of the "Sprint 002" menu
+ Then I should see the burndown chart for sprint "Sprint 002"
+
+ @javascript
+ Scenario: View the burndown chart from the taskboard
+ Given I am on the taskboard for "Sprint 002"
+ When I follow "Burndown Chart"
+ Then I should see the burndown chart for sprint "Sprint 002"
+
+ @javascript
+ Scenario: View sprint stories in the work_packages tab
+ Given I am on the master backlog
+ When I view the stories of Sprint 001 in the work_packages tab
+ Then I should be on the work packages index page of the project called "ecookbook"
+ When I press "Filter"
+ Then I should see "Sprint 001" within "#filter_fixed_version_id"
+
+ @javascript
+ Scenario: View the project stories in the work_packages tab
+ Given I am on the master backlog
+ When I view the stories in the work_packages tab
+ Then I should be on the work packages index page of the project called "ecookbook"
+ When I press "Filter"
+ Then I should see "Version" within "#filters"
+
+ Scenario: Copy estimate to remaining
+ Given I am on the taskboard for "Sprint 001"
+ And I want to create a task for Story 1
+ And I set the subject of the task to A Whole New Task
+ And I set the estimated_hours of the task to 3
+ When I create the task
+ Then the request should complete successfully
+ And task A Whole New Task should have remaining_hours set to 3
+
+ Scenario: Copy remaining to estimate
+ Given I am on the taskboard for "Sprint 001"
+ And I want to create a task for Story 1
+ And I set the subject of the task to A Whole New Task
+ And I set the remaining_hours of the task to 3
+ When I create the task
+ Then the request should complete successfully
+ And task A Whole New Task should have estimated_hours set to 3
+
+ Scenario: Set both estimate and remaining
+ Given I am on the taskboard for "Sprint 001"
+ And I want to create a task for Story 1
+ And I set the subject of the task to A Whole New Task
+ And I set the remaining_hours of the task to 3
+ And I set the estimated_hours of the task to 8
+ When I create the task
+ And I want to create a task for Story 1
+ And I set the subject of the task to A Second New Task
+ And I set the remaining_hours of the task to 1
+ And I set the estimated_hours of the task to 2
+ When I create the task
+ Then the request should complete successfully
+ And task A Whole New Task should have remaining_hours set to 3
+ And task A Whole New Task should have estimated_hours set to 8
+ And story Story 1 should have remaining_hours set to 4
+ And story Story 1 should have estimated_hours set to 10
diff --git a/vendored-plugins/openproject-backlogs/features/user_settings.feature b/vendored-plugins/openproject-backlogs/features/user_settings.feature
new file mode 100644
index 0000000000..c221f0fe32
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/user_settings.feature
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: User settings
+
+ Scenario: A user can change its task color
+ Given there is 1 user with the following:
+ | login | bob |
+ And I am already logged in as "bob"
+ And I go to the my settings page
+ And I fill in "Task color" with "#FBC4B3"
+ And I submit the form by the "Save" button
+ Then I should see "Account was successfully updated"
+ And the "Task color" field should contain "#FBC4B3"
diff --git a/vendored-plugins/openproject-backlogs/features/version_settings.feature b/vendored-plugins/openproject-backlogs/features/version_settings.feature
new file mode 100644
index 0000000000..86ffac5e9d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/features/version_settings.feature
@@ -0,0 +1,151 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Version Settings
+ As a Project Admin
+ I want to configure the backlogs plugin
+ So that my team and I can work effectively
+
+ Background:
+ Given there is 1 project with:
+ | name | ecookbook |
+ And I am working in project "ecookbook"
+ And the project uses the following modules:
+ | backlogs |
+ And the backlogs module is initialized
+ And there is 1 user with:
+ | login | padme |
+ And there is a role "project admin"
+ And the role "project admin" may have the following rights:
+ | manage_versions |
+ | view_master_backlog |
+ And the user "padme" is a "project admin"
+ And there is a default status with:
+ | name | new |
+ And the project has the following sprints:
+ | name | start_date | effective_date |
+ | Sprint 001 | 2010-01-01 | 2010-01-31 |
+ | Sprint 002 | 2010-02-01 | 2010-02-28 |
+ | Sprint 003 | 2010-03-01 | 2010-03-31 |
+ | Sprint 004 | 2010-03-01 | 2010-03-31 |
+ And I am already logged in as "padme"
+
+ Scenario: Creating a new version
+ When I go to the versions/new page of the project called "ecookbook"
+ And I fill in "version_name" with "Sprint X"
+ And I press "Create"
+ Then I should be on the settings/versions page of the project called "ecookbook"
+ And I should see "Successful creation." within "div.notice"
+ And I should see "Sprint X" within "#tab-content-versions"
+
+ Scenario: One can select whether versions are displayed left or right (left is default) in the backlogs page
+ When I go to the edit page of the version called "Sprint 001"
+
+ Then the editable attributes of the version should be the following:
+ | Column in backlog | left |
+
+ When I select "right" from "Column in backlog"
+ And I press "Save"
+
+ Then I should be on the settings/versions page of the project called "ecookbook"
+
+ Scenario: Inherited versions can also be configured to be displayed left, right or not at all
+ Given there is 1 project with:
+ | name | parent |
+ And I am working in project "parent"
+ And the project uses the following modules:
+ | backlogs |
+ And the project has the following sprints:
+ | name | start_date | effective_date | sharing |
+ | shared | 2010-01-01 | 2010-01-31 | system |
+ And I am working in project "ecookbook"
+
+ When I go to the settings/versions page of the project called "ecookbook"
+
+ Then I should see "Edit" within ".version.shared .buttons"
+
+ When I follow "Edit" within ".version.shared .buttons"
+
+ Then the editable attributes of the version should be the following:
+ | Column in backlog | left |
+
+ When I select "right" from "Column in backlog"
+ And I press "Save"
+
+ Then I should be on the settings/versions page of the project called "ecookbook"
+
+ Scenario: Setting "Column in backlog" to the different settings
+ When I go to the edit page of the version called "Sprint 001"
+ And I select "right" from "Column in backlog"
+ And I press "Save"
+ And I go to the edit page of the version called "Sprint 001"
+
+ Then the editable attributes of the version should be the following:
+ | Column in backlog | right |
+
+ When I go to the edit page of the version called "Sprint 002"
+ And I select "left" from "Column in backlog"
+ And I press "Save"
+ And I go to the edit page of the version called "Sprint 002"
+
+ Then the editable attributes of the version should be the following:
+ | Column in backlog | left |
+
+ When I go to the edit page of the version called "Sprint 003"
+ And I select "none" from "Column in backlog"
+ And I press "Save"
+ And I go to the edit page of the version called "Sprint 003"
+
+ Then the editable attributes of the version should be the following:
+ | Column in backlog | none |
+
+ When I go to the master backlog of the project "ecookbook"
+
+ Then I should see "Sprint 001" within "#owner_backlogs_container"
+ And I should see "Sprint 002" within "#sprint_backlogs_container"
+ And I should not see "Sprint 003" within "#owner_backlogs_container"
+ And I should not see "Sprint 003" within "#sprint_backlogs_container"
+
+ Scenario: There should be a version start date field
+ When I go to the edit page of the version called "Sprint 001"
+ Then the editable attributes of the version should be the following:
+ | Start date | 2010-01-01 |
+
+ Scenario: former sprint_start_date and start_date are the same
+ When I go to the edit page of the version called "Sprint 001"
+ And I fill in "2010-01-20" for "version_start_date"
+ And I press "Save"
+ And I go to the master backlog of the project "ecookbook"
+ Then the start date of "Sprint 001" should be "2010-01-20"
diff --git a/vendored-plugins/openproject-backlogs/frontend/app/openproject-backlogs-app.js b/vendored-plugins/openproject-backlogs/frontend/app/openproject-backlogs-app.js
new file mode 100644
index 0000000000..e5c00b7279
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/frontend/app/openproject-backlogs-app.js
@@ -0,0 +1,54 @@
+//-- copyright
+// OpenProject Backlogs Plugin
+//
+// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+// Copyright (C)2010-2011 friflaj
+// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+// Copyright (C)2009-2010 Mark Maglana
+// Copyright (C)2009 Joe Heck, Nate Lowrie
+//
+// This program is free software; you can redistribute it and/or modify it under
+// the terms of the GNU General Public License version 3.
+//
+// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+// The copyright follows:
+// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+// load all js locales
+var localeFiles = require.context('../../config/locales', false, /js-[\w|-]{2,5}\.yml$/);
+localeFiles.keys().forEach(function(localeFile) {
+ var locale = localeFile.match(/js-([\w|-]{2,5})\.yml/)[1];
+ I18n.addTranslations(locale, localeFiles(localeFile)[locale]);
+});
+
+// main app
+var openprojectBacklogsApp = angular.module('openproject');
+
+openprojectBacklogsApp.run([ 'ConfigurationService',
+ 'WorkPackagesOverviewService',
+ function(ConfigurationService, WorkPackagesOverviewService) {
+ if (ConfigurationService.isModuleEnabled('backlogs')) {
+ WorkPackagesOverviewService.addAttributesToGroup('estimatesAndTime', ['storyPoints', 'remainingTime']);
+ }
+}]);
+
+require('jquery-ui/ui/jquery-ui.js');
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs.rb
new file mode 100644
index 0000000000..7309b4b8e3
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module Backlogs
+ require 'open_project/backlogs/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series.rb
new file mode 100644
index 0000000000..f683ecea5c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Burndown
+ class Series < Array
+ def initialize(*args)
+ @unit = args.pop
+ @name = args.pop.to_sym
+ @display = true
+
+ raise "Unsupported unit '#{@unit}'" unless [:points, :hours].include? @unit
+ super(*args)
+ end
+
+ attr_reader :unit
+ attr_reader :name
+ attr_accessor :display
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series_raw_data.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series_raw_data.rb
new file mode 100644
index 0000000000..9b742643cc
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/burndown/series_raw_data.rb
@@ -0,0 +1,199 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Burndown
+ class SeriesRawData < Hash
+ def initialize(*args)
+ @collect = args.pop
+ @sprint = args.pop
+ @project = args.pop
+ super(*args)
+ end
+
+ attr_reader :collect
+ attr_reader :sprint
+ attr_reader :project
+
+ def collect_names
+ @names ||= @collect.to_a.map(&:last).flatten
+ end
+
+ def unit_for(name)
+ return :points if @collect[:points].include? name
+ end
+
+ def collect_data
+ initialize_self_for_collection
+
+ data_for_dates(collected_days).each do |day_data|
+ date = day_data['date']
+ date = date.is_a?(Date) ? date : Date.parse(date)
+
+ day_data.each do |key, value|
+ next if key == 'date'
+
+ self[key][date] = value.to_f
+ end
+ end
+ end
+
+ private
+
+ def initialize_self_for_collection
+ date_hash = {}
+
+ collected_days.each do |date|
+ date_hash[date] = 0.0
+ end
+
+ collect_names.each do |c|
+ self[c] = date_hash.dup
+ end
+ end
+
+ def collected_days
+ @collected_days ||= begin
+ days = sprint.days(nil)
+ days.sort.select { |d| d <= Date.today }
+ end
+ end
+
+ def data_for_dates(dates)
+ return [] if dates.empty?
+ query_string = <<-SQL
+ SELECT
+ date_journals.date,
+ SUM(work_package_journals.story_points) as story_points
+ FROM
+ (
+ #{authoritative_journal_for_date(dates)}
+ ) AS date_journals
+ JOIN journals AS id_journals
+ ON date_journals.journable_id = id_journals.journable_id
+ AND date_journals.version = id_journals.version
+ AND id_journals.journable_type = 'WorkPackage'
+ LEFT JOIN work_package_journals
+ ON work_package_journals.journal_id = id_journals.id
+ AND #{fixed_version_query}
+ AND #{project_id_query}
+ AND #{type_id_query}
+ AND #{status_query}
+ GROUP BY date_journals.date
+ ORDER BY date_journals.date
+ SQL
+
+ Journal::WorkPackageJournal.connection.select_all query_string
+ end
+
+ def authoritative_journal_for_date(dates)
+ raise 'dates must not be empty!' if dates.empty?
+
+ query = <<-SQL
+ SELECT
+ d.date,
+ j.journable_id,
+ MAX(j.version) as version
+ FROM
+ (
+ #{dates_of_interest_join_table(dates)}
+ ) as d
+ JOIN
+ (
+ SELECT
+ CAST(j.created_at AS DATE) AS created_at,
+ j.journable_id,
+ MAX(j.version) as version
+ FROM
+ journals AS j
+ GROUP BY
+ CAST(j.created_at AS DATE),
+ j.journable_type,
+ j.journable_id
+ HAVING j.journable_type = 'WorkPackage'
+ ORDER BY j.journable_id, version
+ ) as j
+ ON d.date >= j.created_at
+ GROUP BY d.date, j.journable_id
+ ORDER BY j.journable_id, d.date, version
+ SQL
+
+ query
+ end
+
+ def dates_of_interest_join_table(dates)
+ raise 'dates must not be empty!' if dates.empty?
+
+ @date_join ||= dates.map { |date|
+ "SELECT CAST('#{date}' AS DATE) AS date"
+ }.join(' UNION ')
+ end
+
+ def status_query
+ @status_query ||= begin
+ non_closed_statuses = Status.where(is_closed: false).select(:id).map(&:id)
+
+ done_statuses_for_project = project.done_statuses.select(:id).map(&:id)
+
+ open_status_ids = non_closed_statuses - done_statuses_for_project
+
+ "(#{Journal::WorkPackageJournal.table_name}.status_id IN (#{open_status_ids.join(',')}))"
+ end
+ end
+
+ def fixed_version_query
+ @fixed_version_query ||= "(#{Journal::WorkPackageJournal.table_name}.fixed_version_id = #{sprint.id})"
+ end
+
+ def project_id_query
+ @project_id_query ||= "(#{Journal::WorkPackageJournal.table_name}.project_id = #{project.id})"
+ end
+
+ def type_id_query
+ @type_id_query ||= "(#{Journal::WorkPackageJournal.table_name}.type_id in (#{collected_types.join(',')}))"
+ end
+
+ def ignore_if_has_parent
+ @ignore_if_has_parent ||= "(#{Journal::WorkPackageJournal.table_name}.parent_id IS NOT NULL)"
+ end
+
+ def collected_from_children?(key, story)
+ key == 'remaining_hours' && story_has_children?(story)
+ end
+
+ def collected_types
+ @collected_types ||= Story.types << Task.type
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/compatibility.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/compatibility.rb
new file mode 100644
index 0000000000..20ecbb1bc0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/compatibility.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Compatibility
+ def using_jquery?
+ OpenProject::Compatibility.respond_to?(:using_jquery?) and
+ OpenProject::Compatibility.using_jquery?
+ rescue NameError
+ false
+ end
+
+ extend self
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/engine.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/engine.rb
new file mode 100644
index 0000000000..3f0780893e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/engine.rb
@@ -0,0 +1,203 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/plugins'
+
+require 'acts_as_silent_list'
+
+module OpenProject::Backlogs
+ class Engine < ::Rails::Engine
+ engine_name :openproject_backlogs
+
+ def self.settings
+ { default: { 'story_types' => nil,
+ 'task_type' => nil,
+ 'card_spec' => nil
+ },
+ partial: 'shared/settings' }
+ end
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-backlogs',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0',
+ settings: settings do
+ Redmine::AccessControl.permission(:edit_project).actions << 'projects/project_done_statuses'
+ Redmine::AccessControl.permission(:edit_project).actions << 'projects/rebuild_positions'
+
+ project_module :backlogs do
+ # SYNTAX: permission :name_of_permission, { :controller_name => [:action1, :action2] }
+
+ # Master backlog permissions
+ permission :view_master_backlog, rb_master_backlogs: :index,
+ rb_sprints: [:index, :show],
+ rb_wikis: :show,
+ rb_stories: [:index, :show],
+ rb_queries: :show,
+ rb_burndown_charts: :show,
+ rb_export_card_configurations: [:index, :show]
+
+ permission :view_taskboards, rb_taskboards: :show,
+ rb_sprints: :show,
+ rb_stories: :show,
+ rb_tasks: [:index, :show],
+ rb_impediments: [:index, :show],
+ rb_wikis: :show,
+ rb_burndown_charts: :show,
+ rb_export_card_configurations: [:index, :show]
+
+ # Sprint permissions
+ # :show_sprints and :list_sprints are implicit in :view_master_backlog permission
+ permission :update_sprints, rb_sprints: [:edit, :update],
+ rb_wikis: [:edit, :update]
+
+ # Story permissions
+ # :show_stories and :list_stories are implicit in :view_master_backlog permission
+ permission :create_stories, rb_stories: :create
+ permission :update_stories, rb_stories: :update
+
+ # Task permissions
+ # :show_tasks and :list_tasks are implicit in :view_sprints
+ permission :create_tasks, rb_tasks: [:new, :create]
+ permission :update_tasks, rb_tasks: [:edit, :update]
+
+ # Impediment permissions
+ # :show_impediments and :list_impediments are implicit in :view_sprints
+ permission :create_impediments, rb_impediments: [:new, :create]
+ permission :update_impediments, rb_impediments: [:edit, :update]
+ end
+
+ menu :project_menu,
+ :backlogs,
+ { controller: '/rb_master_backlogs', action: :index },
+ caption: :project_module_backlogs,
+ before: :calendar,
+ param: :project_id,
+ if: proc { not(User.current.respond_to?(:impaired?) and User.current.impaired?) },
+ html: { class: 'icon2 icon-backlogs' }
+ end
+
+ assets %w(
+ backlogs/backlogs.css
+ backlogs/backlogs.js
+ backlogs/master_backlog.css
+ backlogs/taskboard.css
+ backlogs/jquery.flot/excanvas.js
+ backlogs/burndown.js
+ )
+
+ patches [:PermittedParams,
+ :WorkPackage,
+ :Status,
+ :Type,
+ :MyController,
+ :Project,
+ :ProjectsController,
+ :ProjectsHelper,
+ :Query,
+ :User,
+ :VersionsController,
+ :Version]
+
+ patch_with_namespace :API, :V3, :WorkPackages, :Schema, :SpecificWorkPackageSchema
+ patch_with_namespace :BasicData, :SettingSeeder
+
+ extend_api_response(:v3, :work_packages, :work_package) do
+ property :story_points,
+ render_nil: true,
+ if: -> (*) { backlogs_enabled? && type.story? }
+
+ property :remaining_time,
+ exec_context: :decorator,
+ getter: -> (*) {
+ datetime_formatter.format_duration_from_hours(represented.remaining_hours,
+ allow_nil: true)
+ },
+ render_nil: true,
+ if: -> (*) { represented.backlogs_enabled? }
+ end
+
+ extend_api_response(:v3, :work_packages, :work_package_payload) do
+ property :story_points,
+ render_nil: true,
+ if: -> (*) { backlogs_enabled? && type.story? }
+
+ property :remaining_time,
+ exec_context: :decorator,
+ getter: -> (*) {
+ datetime_formatter.format_duration_from_hours(represented.remaining_hours,
+ allow_nil: true)
+ },
+ setter: -> (value, *) {
+ remaining = datetime_formatter.parse_duration_to_hours(value,
+ 'remainingTime',
+ allow_nil: true)
+ represented.remaining_hours = remaining
+ },
+ render_nil: true,
+ if: -> (*) { represented.backlogs_enabled? }
+ end
+
+ extend_api_response(:v3, :work_packages, :schema, :work_package_schema) do
+ schema :story_points,
+ type: 'Integer',
+ required: false,
+ show_if: -> (*) {
+ represented.project.backlogs_enabled? &&
+ (!represented.type || represented.type.story?)
+ }
+
+ schema :remaining_time,
+ type: 'Duration',
+ name_source: :remaining_hours,
+ required: false,
+ show_if: -> (*) { represented.project.backlogs_enabled? }
+ end
+
+ add_api_attribute on: :work_package, ar_name: :story_points
+ add_api_attribute on: :work_package, ar_name: :remaining_hours, api_name: :remaining_time do
+ if !model.new_record? &&
+ !model.leaf? &&
+ model.changed.include?('remaining_hours')
+ errors.add :error_readonly, 'remaining_hours'
+ end
+ end
+
+ initializer 'backlogs.register_hooks' do
+ require 'open_project/backlogs/hooks'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/hooks.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/hooks.rb
new file mode 100644
index 0000000000..a11540c5b0
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/hooks.rb
@@ -0,0 +1,161 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Hooks
+ class Hook < Redmine::Hook::Listener
+ include ActionView::Helpers::TagHelper
+ include ActionView::Context
+ include WorkPackagesHelper
+
+ def work_packages_show_attributes(context = {})
+ work_package = context[:work_package]
+ attributes = context[:attributes]
+
+ return unless work_package.backlogs_enabled?
+ return if context[:from] == 'OpenProject::Backlogs::WorkPackageView::FieldsParagraph'
+
+ attributes << work_package_show_story_points_attribute(work_package)
+ attributes << work_package_show_remaining_hours_attribute(work_package)
+
+ attributes
+ end
+
+ private
+
+ def work_package_show_story_points_attribute(work_package)
+ return nil unless work_package.is_story?
+
+ work_package_show_table_row(:story_points, :"story-points") do
+ work_package.story_points ? work_package.story_points.to_s : empty_element_tag
+ end
+ end
+
+ def work_package_show_remaining_hours_attribute(work_package)
+ work_package_show_table_row(:remaining_hours) do
+ work_package.remaining_hours ? l_hours(work_package.remaining_hours) : empty_element_tag
+ end
+ end
+ end
+
+ class LayoutHook < Redmine::Hook::ViewListener
+ include RbCommonHelper
+
+ # TODO: there are hook implementations in here that need to be ported
+ # to render a partial instead of returning a hand crafted snippet
+
+ render_on :view_work_packages_form_details_bottom,
+ partial: 'hooks/backlogs/view_work_packages_form_details_bottom'
+
+ def view_versions_show_bottom(context = {})
+ version = context[:version]
+ project = version.project
+
+ return '' unless project.module_enabled? 'backlogs'
+
+ snippet = ''
+
+ if User.current.allowed_to?(:edit_wiki_pages, project)
+ snippet += ''
+ snippet += link_to l(:button_edit_wiki), { controller: '/rb_wikis', action: 'edit', project_id: project.id, sprint_id: version.id }, class: 'icon icon-edit'
+ snippet += ' '
+
+ # This wouldn't be necesary if the schedules plugin didn't disable the
+ # contextual hook
+ snippet += javascript_tag(<<-JS)
+ (function ($) {
+ $(document).ready(function() {
+ $('#edit_wiki_page_action').detach().appendTo("div.contextual");
+ });
+ }(jQuery))
+ JS
+ end
+ end
+
+ def view_my_settings(context = {})
+ context[:controller].send(
+ :render_to_string,
+ partial: 'shared/view_my_settings',
+ locals: {
+ user: context[:user],
+ color: context[:user].backlogs_preference(:task_color),
+ versions_default_fold_state:
+ context[:user].backlogs_preference(:versions_default_fold_state) })
+ end
+
+ def controller_work_package_new_after_save(context = {})
+ params = context[:params]
+ work_package = context[:work_package]
+
+ return unless work_package.backlogs_enabled?
+
+ if work_package.is_story?
+ if params[:link_to_original]
+ rel = Relation.new
+
+ rel.from_id = Integer(params[:link_to_original])
+ rel.to_id = work_package.id
+ rel.relation_type = Relation::TYPE_RELATES
+ rel.save
+ end
+
+ if params[:copy_tasks]
+ params[:copy_tasks] += ':' if params[:copy_tasks] !~ /:/
+ action, id = *(params[:copy_tasks].split(/:/))
+
+ story = (id.nil? ? nil : Story.find(Integer(id)))
+
+ if !story.nil? && action != 'none'
+ tasks = story.tasks
+ case action
+ when 'open'
+ tasks = tasks.select { |t| !t.closed? }
+ when 'all', 'none'
+ #
+ else
+ raise "Unexpected value #{params[:copy_tasks]}"
+ end
+
+ tasks.each {|t|
+ nt = Task.new
+ nt.copy_from(t)
+ nt.parent_id = work_package.id
+ nt.save
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/list.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/list.rb
new file mode 100644
index 0000000000..25c0f8a6b5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/list.rb
@@ -0,0 +1,153 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::List
+ def self.included(base)
+ base.class_eval do
+ acts_as_silent_list
+
+ # Reorder list, if work_package is removed from sprint
+ before_update :fix_other_work_package_positions
+ before_update :fix_own_work_package_position
+
+ # Used by acts_as_silent_list to limit the list to a certain subset within
+ # the table.
+ #
+ # Also sanitize_sql seems to be unavailable in a sensible way. Therefore
+ # we're using send to circumvent visibility work_packages.
+ def scope_condition
+ self.class.send(:sanitize_sql, ['project_id = ? AND fixed_version_id = ? AND type_id IN (?)',
+ project_id, fixed_version_id, types])
+ end
+
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def move_after(prev_id)
+ # Remove so the potential 'prev' has a correct position
+ remove_from_list
+ reload
+
+ prev = self.class.find_by_id(prev_id.to_i)
+
+ # If it should be the first story, move it to the 1st position
+ if prev.blank?
+ insert_at
+ move_to_top
+
+ # If its predecessor has no position, create an order on position
+ # silently. This can happen when sorting inside a version for the first
+ # time after backlogs was activated and there have already been items
+ # inside the version at the time of backlogs activation
+ elsif !prev.in_list?
+ prev_pos = set_default_prev_positions_silently(prev)
+ insert_at(prev_pos += 1)
+
+ # There's a valid predecessor
+ else
+ insert_at(prev.position + 1)
+ end
+ end
+
+ protected
+
+ def fix_other_work_package_positions
+ if changes.slice('project_id', 'type_id', 'fixed_version_id').present?
+ if changes.slice('project_id', 'fixed_version_id').blank? and
+ Story.types.include?(type_id.to_i) and
+ Story.types.include?(type_id_was.to_i)
+ return
+ end
+
+ if fixed_version_id_changed?
+ restore_version_id = true
+ new_version_id = fixed_version_id
+ self.fixed_version_id = fixed_version_id_was
+ end
+
+ if type_id_changed?
+ restore_type_id = true
+ new_type_id = type_id
+ self.type_id = type_id_was
+ end
+
+ if project_id_changed?
+ restore_project_id = true
+ # I've got no idea, why there's a difference between setting the
+ # project via project= or via project_id=, but there is.
+ new_project = project
+ self.project = Project.find(project_id_was)
+ end
+
+ remove_from_list if is_story?
+
+ if restore_project_id
+ self.project = new_project
+ end
+
+ if restore_type_id
+ self.type_id = new_type_id
+ end
+
+ if restore_version_id
+ self.fixed_version_id = new_version_id
+ end
+ end
+ end
+
+ def fix_own_work_package_position
+ if changes.slice('project_id', 'type_id', 'fixed_version_id').present?
+ if changes.slice('project_id', 'fixed_version_id').blank? and
+ Story.types.include?(type_id.to_i) and
+ Story.types.include?(type_id_was.to_i)
+ return
+ end
+
+ if is_story? and fixed_version.present?
+ insert_at_bottom
+ else
+ assume_not_in_list
+ end
+ end
+ end
+
+ def set_default_prev_positions_silently(prev)
+ prev.fixed_version.rebuild_positions(prev.project)
+ prev.reload.position
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/mixins/prevent_issue_sti.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/mixins/prevent_issue_sti.rb
new file mode 100644
index 0000000000..68a0886991
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/mixins/prevent_issue_sti.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Mixins
+ module PreventIssueSti
+ # Overrides ActiveRecord::Inheritance::ClassMethods#sti_name
+ # so that stories are stored and found with type-attribute = "WorkPackage"
+ def sti_name
+ 'WorkPackage'
+ end
+
+ # Overrides ActiveRecord::Inheritance::ClassMethods#find_sti_classes
+ # so that stories are instantiated correctly despite sti_name beeing "WorkPackage"
+ def find_sti_class(type_name)
+ type_name = to_s if type_name == 'WorkPackage'
+
+ super(type_name)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches.rb
new file mode 100644
index 0000000000..1ff6b802c8
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Patches
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/my_controller_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/my_controller_patch.rb
new file mode 100644
index 0000000000..373e3a1b71
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/my_controller_patch.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'my_controller'
+
+module OpenProject::Backlogs::Patches::MyControllerPatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+
+ after_filter :save_backlogs_preferences, only: [:settings]
+ end
+ end
+
+ module InstanceMethods
+ def save_backlogs_preferences
+ if request.patch? && flash[:notice] == l(:notice_account_updated)
+ versions_default_fold_state = (params[:backlogs] && params[:backlogs][:versions_default_fold_state]) ? params[:backlogs][:versions_default_fold_state] : 'open'
+ User.current.backlogs_preference(:versions_default_fold_state, versions_default_fold_state)
+
+ color = (params[:backlogs] ? params[:backlogs][:task_color] : '').to_s
+ if color == '' || color.match(/^#[A-Fa-f0-9]{6}$/)
+ User.current.backlogs_preference(:task_color, color)
+ else
+ flash[:notice] = "Invalid task color code #{color}"
+ end
+ end
+ end
+ end
+end
+
+MyController.send(:include, OpenProject::Backlogs::Patches::MyControllerPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/permitted_params_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/permitted_params_patch.rb
new file mode 100644
index 0000000000..007fd2e557
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/permitted_params_patch.rb
@@ -0,0 +1,68 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'permitted_params'
+
+module OpenProject::Backlogs::Patches::PermittedParamsPatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ alias_method_chain :new_work_package, :backlogs
+ alias_method_chain :update_work_package, :backlogs
+ end
+ end
+
+ module InstanceMethods
+ def new_work_package_with_backlogs(args = {})
+ permitted_params = new_work_package_without_backlogs(args)
+
+ backlogs_params = params.require(:work_package).permit(:story_points, :remaining_hours)
+ permitted_params.merge!(backlogs_params)
+
+ permitted_params
+ end
+
+ def update_work_package_with_backlogs(args = {})
+ permitted_params = update_work_package_without_backlogs(args)
+
+ backlogs_params = params.require(:work_package).permit(:story_points, :remaining_hours)
+ permitted_params.merge!(backlogs_params)
+
+ permitted_params
+ end
+ end
+end
+PermittedParams.send(:include, OpenProject::Backlogs::Patches::PermittedParamsPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/project_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/project_patch.rb
new file mode 100644
index 0000000000..cbde7abe27
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/project_patch.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'project'
+
+module OpenProject::Backlogs::Patches::ProjectPatch
+ def self.included(base)
+ base.class_eval do
+ has_and_belongs_to_many :done_statuses, join_table: :done_statuses_for_project, class_name: 'Status'
+
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def rebuild_positions
+ return unless backlogs_enabled?
+
+ shared_versions.each do |v| v.rebuild_positions(self) end
+ nil
+ end
+
+ def backlogs_enabled?
+ module_enabled? 'backlogs'
+ end
+ end
+end
+
+Project.send(:include, OpenProject::Backlogs::Patches::ProjectPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_controller_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_controller_patch.rb
new file mode 100644
index 0000000000..f147f89b4d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_controller_patch.rb
@@ -0,0 +1,83 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'projects_controller'
+
+module OpenProject::Backlogs::Patches::ProjectsControllerPatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+
+ alias_method_chain :load_project_settings, :backlogs_settings
+ end
+ end
+
+ module InstanceMethods
+ def load_project_settings_with_backlogs_settings
+ load_project_settings_without_backlogs_settings
+ @statuses = Status.all
+ end
+
+ def project_done_statuses
+ selected_statuses = (params[:statuses] || []).map { |work_package_status|
+ Status.find(work_package_status[:status_id].to_i)
+ }.compact
+
+ @project.done_statuses = selected_statuses
+ @project.save!
+
+ flash[:notice] = l(:notice_successful_update)
+
+ redirect_to action: 'settings', id: @project, tab: 'backlogs_settings'
+ end
+
+ def rebuild_positions
+ @project.rebuild_positions
+ flash[:notice] = l('backlogs.positions_rebuilt_successfully')
+
+ redirect_to action: 'settings', id: @project, tab: 'backlogs_settings'
+ rescue ActiveRecord::ActiveRecordError
+ flash[:error] = l('backlogs.positions_could_not_be_rebuilt')
+
+ logger.error("Tried to rebuild positions for project #{@project.identifier.inspect} but could not...")
+ logger.error($!)
+ logger.error($@)
+
+ redirect_to action: 'settings', id: @project, tab: 'backlogs_settings'
+ end
+ end
+end
+
+ProjectsController.send(:include, OpenProject::Backlogs::Patches::ProjectsControllerPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_helper_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_helper_patch.rb
new file mode 100644
index 0000000000..adb28cbbf2
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/projects_helper_patch.rb
@@ -0,0 +1,59 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'projects_helper'
+
+module OpenProject::Backlogs::Patches::ProjectsHelperPatch
+ def self.included(base)
+ base.module_eval do
+ def project_settings_tabs_with_backlogs_settings
+ project_settings_tabs_without_backlogs_settings.tap do |settings|
+ if @project.module_enabled? 'backlogs'
+ settings << {
+ name: 'backlogs_settings',
+ action: :manage_project_activities,
+ partial: 'projects/settings/backlogs_settings',
+ label: 'backlogs.backlog_settings'
+ }
+ end
+ end
+ end
+
+ alias_method_chain :project_settings_tabs, :backlogs_settings
+ end
+ end
+end
+
+ProjectsHelper.send(:include, OpenProject::Backlogs::Patches::ProjectsHelperPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/query_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/query_patch.rb
new file mode 100644
index 0000000000..732c7f0f07
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/query_patch.rb
@@ -0,0 +1,134 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'query'
+
+module OpenProject::Backlogs::Patches::QueryPatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+
+ add_available_column(QueryColumn.new(:story_points,
+ sortable: "#{WorkPackage.table_name}.story_points",
+ summable: true))
+ add_available_column(QueryColumn.new(:remaining_hours,
+ sortable: "#{WorkPackage.table_name}.remaining_hours",
+ summable: true))
+
+ add_available_column(QueryColumn.new(:position,
+ default_order: 'asc',
+ # Sort by position only, always show work_packages without a position at the end
+ sortable: "CASE WHEN #{WorkPackage.table_name}.position IS NULL THEN 1 ELSE 0 END ASC, #{WorkPackage.table_name}.position"
+ ))
+ Queries::WorkPackages::Filter.add_filter_type_by_field('backlogs_work_package_type', 'list')
+
+ alias_method_chain :available_work_package_filters, :backlogs_work_package_type
+ alias_method_chain :sql_for_field, :backlogs_work_package_type
+ end
+ end
+
+ module InstanceMethods
+ def available_work_package_filters_with_backlogs_work_package_type
+ available_work_package_filters_without_backlogs_work_package_type.tap do |filters|
+ if backlogs_configured? and backlogs_enabled?
+ filters['backlogs_work_package_type'] = {
+ type: :list,
+ values: [[l(:story, scope: [:backlogs]), 'story'],
+ [l(:task, scope: [:backlogs]), 'task'],
+ [l(:impediment, scope: [:backlogs]), 'impediment'],
+ [l(:any, scope: [:backlogs]), 'any']],
+ order: 20
+ }
+ end
+ end
+ end
+
+ def sql_for_field_with_backlogs_work_package_type(field, operator, v, db_table, db_field, is_custom_filter = false)
+ if field == 'backlogs_work_package_type'
+ db_table = WorkPackage.table_name
+
+ sql = []
+
+ selected_values = values_for(field)
+ selected_values = ['story', 'task'] if selected_values.include?('any')
+
+ story_types = Story.types.map { |val| "#{val}" }.join(',')
+ all_types = (Story.types + [Task.type]).map { |val| "#{val}" }.join(',')
+
+ selected_values.each do |val|
+ case val
+ when 'story'
+ sql << "(#{db_table}.type_id IN (#{story_types}))"
+ when 'task'
+ sql << "(#{db_table}.type_id = #{Task.type} AND NOT #{db_table}.parent_id IS NULL)"
+ when 'impediment'
+ sql << "(#{db_table}.id IN (
+ select from_id
+ FROM relations ir
+ JOIN work_packages blocked
+ ON
+ blocked.id = ir.to_id
+ AND blocked.type_id IN (#{all_types})
+ WHERE ir.relation_type = 'blocks'
+ ) AND #{db_table}.parent_id IS NULL)"
+ end
+ end
+
+ case operator
+ when '='
+ sql = sql.join(' OR ')
+ when '!'
+ sql = 'NOT (' + sql.join(' OR ') + ')'
+ end
+
+ sql
+ else
+ sql_for_field_without_backlogs_work_package_type(field, operator, v, db_table, db_field, is_custom_filter)
+ end
+ end
+
+ protected
+
+ def backlogs_configured?
+ Story.types.present? and Task.type.present?
+ end
+
+ def backlogs_enabled?
+ project.blank? or project.module_enabled?(:backlogs)
+ end
+ end
+end
+
+Query.send(:include, OpenProject::Backlogs::Patches::QueryPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/setting_seeder_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/setting_seeder_patch.rb
new file mode 100644
index 0000000000..74208f7744
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/setting_seeder_patch.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Backlogs::Patches::SettingSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def data
+ original_data = super
+
+ unless original_data['default_projects_modules'].include? 'backlogs'
+ original_data['default_projects_modules'] << 'backlogs'
+ end
+
+ original_data
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/specific_work_package_schema_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/specific_work_package_schema_patch.rb
new file mode 100644
index 0000000000..445b0f48fe
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/specific_work_package_schema_patch.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'api/v3/work_packages/schema/specific_work_package_schema'
+
+module OpenProject::Backlogs::Patches::SpecificWorkPackageSchemaPatch
+ def self.included(base)
+ base.class_eval do
+ prepend InstanceMethods
+ extend ClassMethods
+ end
+ end
+
+ module ClassMethods
+ end
+
+ module InstanceMethods
+ def writable?(property)
+ if property == :remaining_time
+ return @work_package.leaf?
+ end
+
+ super
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/status_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/status_patch.rb
new file mode 100644
index 0000000000..d845c7b9ec
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/status_patch.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'status'
+
+module OpenProject::Backlogs::Patches::StatusPatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def is_done?(project)
+ project.done_statuses.include?(self)
+ end
+ end
+end
+
+Status.send(:include, OpenProject::Backlogs::Patches::StatusPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/type_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/type_patch.rb
new file mode 100644
index 0000000000..0c8d26b835
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/type_patch.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'type'
+
+module OpenProject::Backlogs::Patches::TypePatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+ extend ClassMethods
+ end
+ end
+
+ module ClassMethods
+ end
+
+ module InstanceMethods
+ def story?
+ Story.types.include?(id)
+ end
+
+ def task?
+ Task.type.present? && id == Task.type
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/user_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/user_patch.rb
new file mode 100644
index 0000000000..a7d13dd1bd
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/user_patch.rb
@@ -0,0 +1,88 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'user'
+
+module OpenProject::Backlogs::Patches::UserPatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def backlogs_preference(attr, new_value = nil)
+ setting = read_backlogs_preference(attr)
+
+ if setting.nil? and new_value.nil?
+ new_value = compute_backlogs_preference(attr)
+ end
+
+ if new_value.present?
+ setting = write_backlogs_preference(attr, new_value)
+ end
+
+ setting
+ end
+
+ protected
+
+ def read_backlogs_preference(attr)
+ setting = pref[:"backlogs_#{attr}"]
+
+ setting.blank? ? nil : setting
+ end
+
+ def write_backlogs_preference(attr, new_value)
+ pref[:"backlogs_#{attr}"] = new_value
+ pref.save! unless self.new_record?
+
+ new_value
+ end
+
+ def compute_backlogs_preference(attr)
+ case attr
+ when :task_color
+ ('#%0.6x' % rand(0xFFFFFF)).upcase
+ when :versions_default_fold_state
+ 'open'
+ else
+ raise "Unsupported attribute '#{attr}'"
+ end
+ end
+ end
+end
+
+User.send(:include, OpenProject::Backlogs::Patches::UserPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/version_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/version_patch.rb
new file mode 100644
index 0000000000..3b2e89329e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/version_patch.rb
@@ -0,0 +1,90 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'version'
+
+module OpenProject::Backlogs::Patches::VersionPatch
+ def self.included(base)
+ base.class_eval do
+ has_many :version_settings, dependent: :destroy
+ accepts_nested_attributes_for :version_settings
+
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def rebuild_positions(project = self.project)
+ return unless project.backlogs_enabled?
+
+ WorkPackage.transaction do
+ # Remove position from all non-stories
+ WorkPackage.where(['project_id = ? AND type_id NOT IN (?) AND position IS NOT NULL', project, Story.types])
+ .update_all(position: nil)
+
+ # Add work_packages w/o position to the top of the list and add
+ # work_packages, that have a position, at the end
+ stories_wo_position = fixed_issues.where(project_id: project, type_id: Story.types, position: nil).order('id')
+
+ stories_w_position = fixed_issues.where(project_id: project, type_id: Story.types)
+ .where('position IS NOT NULL')
+ .order('COALESCE(position, 0), id')
+
+ (stories_w_position + stories_wo_position).each_with_index do |story, index|
+ story.send(:update_attribute_silently, 'position', index + 1)
+ end
+ end
+
+ nil
+ end
+
+ def ==(other)
+ super ||
+ other.is_a?(self.class) &&
+ id.present? &&
+ other.id == id
+ end
+
+ def eql?(other)
+ self == other
+ end
+
+ def hash
+ id.hash
+ end
+ end
+end
+
+Version.send(:include, OpenProject::Backlogs::Patches::VersionPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb
new file mode 100644
index 0000000000..c68e2a7009
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb
@@ -0,0 +1,90 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'versions_controller'
+
+module OpenProject::Backlogs::Patches::VersionsControllerPatch
+ def self.included(base)
+ base.class_eval do
+ include VersionSettingsHelper
+ helper :version_settings
+
+ # Find project explicitly on update and edit
+ skip_before_filter :find_project_from_association, only: [:edit, :update]
+ skip_before_filter :find_model_object, only: [:edit, :update]
+ prepend_before_filter :find_project_and_version, only: [:edit, :update]
+
+ before_filter :add_project_to_version_settings_attributes, only: [:update, :create]
+
+ before_filter :whitelist_update_params, only: :update
+
+ def whitelist_update_params
+ if @project != @version.project
+ # Make sure only the version_settings_attributes
+ # (column=left|right|none) can be stored when current project does not
+ # equal the version project (which is valid in inherited versions)
+ if permitted_params.version.present? && permitted_params.version[:version_settings_attributes].present?
+ params['version'] = { version_settings_attributes: permitted_params.version[:version_settings_attributes] }
+ else
+ # This is an unfortunate hack giving how plugins work at the moment.
+ # In this else branch we want the `version` to be an empty hash.
+ permitted_params.define_singleton_method :version, lambda { {} }
+ end
+ end
+ end
+
+ def find_project_and_version
+ find_model_object
+ if params[:project_id]
+ find_project
+ else
+ find_project_from_association
+ end
+ end
+
+ # This forces the current project for the nested version settings in order
+ # to prevent it from being set through firebug etc. #mass_assignment
+ def add_project_to_version_settings_attributes
+ if permitted_params.version['version_settings_attributes'].present?
+ params['version']['version_settings_attributes'].each do |attr_hash|
+ attr_hash['project_id'] = @project.id
+ end
+ end
+ end
+ end
+ end
+end
+
+VersionsController.send(:include, OpenProject::Backlogs::Patches::VersionsControllerPatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/work_package_patch.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/work_package_patch.rb
new file mode 100644
index 0000000000..0a65dca6c1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/patches/work_package_patch.rb
@@ -0,0 +1,264 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'work_package'
+
+module OpenProject::Backlogs::Patches::WorkPackagePatch
+ def self.included(base)
+ base.class_eval do
+ include InstanceMethods
+ extend ClassMethods
+
+ alias_method_chain :recalculate_attributes_for, :remaining_hours
+ before_validation :backlogs_before_validation, if: lambda { |i| i.backlogs_enabled? }
+
+ before_save :inherit_version_from_closest_story_or_impediment, if: lambda { |i| i.is_task? }
+ after_save :inherit_version_to_descendants, if: lambda { |i| (i.fixed_version_id_changed? && i.backlogs_enabled? && i.closest_story_or_impediment == i) }
+ after_move :inherit_version_to_descendants, if: lambda { |i| i.is_task? }
+
+ register_on_journal_formatter(:fraction, 'remaining_hours')
+ register_on_journal_formatter(:decimal, 'story_points')
+ register_on_journal_formatter(:decimal, 'position')
+
+ validates_numericality_of :story_points, only_integer: true,
+ allow_nil: true,
+ greater_than_or_equal_to: 0,
+ less_than: 10_000,
+ if: lambda { |i| i.backlogs_enabled? }
+
+ validates_numericality_of :remaining_hours, only_integer: false,
+ allow_nil: true,
+ greater_than_or_equal_to: 0,
+ if: lambda { |i| i.project && i.project.module_enabled?('backlogs') }
+
+ validate :validate_parent_work_package_relation
+
+ include OpenProject::Backlogs::List
+ end
+ end
+
+ module ClassMethods
+ def backlogs_types
+ # Unfortunately, this is not cachable so the following line would be wrong
+ # @backlogs_types ||= Story.types << Task.type
+ # Caching like in the line above would prevent the types selected
+ # for backlogs to be changed without restarting all app server.
+ (Story.types << Task.type).compact
+ end
+
+ def take_child_update_semaphore
+ @child_updates = true
+ end
+
+ def child_update_semaphore_taken?
+ @child_updates
+ end
+
+ def place_child_update_semaphore
+ @child_updates = false
+ end
+ end
+
+ module InstanceMethods
+ def done?
+ project.done_statuses.to_a.include?(status)
+ end
+
+ def to_story
+ Story.find(id) if is_story?
+ end
+
+ def is_story?
+ backlogs_enabled? && Story.types.include?(type_id)
+ end
+
+ def to_task
+ Task.find(id) if is_task?
+ end
+
+ def is_task?
+ backlogs_enabled? && (parent_id && type_id == Task.type && Task.type.present?)
+ end
+
+ def is_impediment?
+ backlogs_enabled? && (parent_id.nil? && type_id == Task.type && Task.type.present?)
+ end
+
+ def types
+ case
+ when is_story?
+ Story.types
+ when is_task?
+ Task.types
+ else
+ []
+ end
+ end
+
+ def story
+ if self.is_story?
+ return Story.find(id)
+ elsif self.is_task?
+ # Make sure to get the closest ancestor that is a Story, i.e. the one with the highest lft
+ # otherwise, the highest parent that is a Story is returned
+ story_work_package = ancestors.find_by_type_id(Story.types, order: 'lft DESC')
+ return Story.find(story_work_package.id) if story_work_package
+ end
+ nil
+ end
+
+ def blocks
+ # return work_packages that I block that aren't closed
+ return [] if closed?
+ relations_from.map { |ir| ir.relation_type == 'blocks' && !ir.to.closed? ? ir.to : nil }.compact
+ end
+
+ def blockers
+ # return work_packages that block me
+ return [] if closed?
+ relations_to.map { |ir| ir.relation_type == 'blocks' && !ir.from.closed? ? ir.from : nil }.compact
+ end
+
+ def recalculate_attributes_for_with_remaining_hours(work_package_id)
+ if work_package_id.is_a? WorkPackage
+ p = work_package_id
+ else
+ p = WorkPackage.find_by(id: work_package_id)
+ end
+
+ if p.present?
+ if backlogs_enabled? &&
+ p.left != (p.right + 1) # this node has children
+
+ p.remaining_hours = p.leaves.sum(:remaining_hours).to_f
+ p.remaining_hours = nil if p.remaining_hours == 0.0
+ end
+
+ recalculate_attributes_for_without_remaining_hours(p)
+ end
+ end
+
+ def inherit_version_from(source)
+ self.fixed_version_id = source.fixed_version_id if source && project_id == source.project_id
+ end
+
+ def backlogs_enabled?
+ !!project.try(:module_enabled?, 'backlogs')
+ end
+
+ def in_backlogs_type?
+ backlogs_enabled? && WorkPackage.backlogs_types.include?(type.try(:id))
+ end
+
+ # ancestors array similar to Module#ancestors
+ # i.e. returns immediate ancestors first
+ def ancestor_chain
+ ancestors = []
+ unless parent_id.nil?
+
+ # Unfortunately the nested set is only build on save hence, the #parent
+ # method is not always correct. Therefore we go to the parent the hard
+ # way and use nested set from there
+ real_parent = WorkPackage.find_by(id: parent_id)
+
+ # Sort immediate ancestors first
+ ancestors = [real_parent] + real_parent.ancestors.includes(project: :enabled_modules).order(:rgt)
+ end
+ ancestors
+ end
+
+ def closest_story_or_impediment
+ return nil unless in_backlogs_type?
+ return self if self.is_story? || self.is_impediment?
+ closest = nil
+ ancestor_chain.each do |i|
+ # break if we found an element in our chain that is not relevant in backlogs
+ break unless i.in_backlogs_type?
+ if i.is_story? || i.is_impediment?
+ closest = i
+ break
+ end
+ end
+ closest
+ end
+
+ private
+
+ def backlogs_before_validation
+ if type_id == Task.type
+ self.estimated_hours = remaining_hours if estimated_hours.blank? && !remaining_hours.blank?
+ self.remaining_hours = estimated_hours if remaining_hours.blank? && !estimated_hours.blank?
+ end
+ end
+
+ def inherit_version_from_closest_story_or_impediment
+ root = closest_story_or_impediment
+ inherit_version_from(root) if root != self
+ end
+
+ def inherit_version_to_descendants
+ if !WorkPackage.child_update_semaphore_taken?
+ begin
+ WorkPackage.take_child_update_semaphore
+
+ descendant_tasks, stop_descendants = descendants.includes(project: :enabled_modules).partition(&:is_task?)
+ descendant_tasks.reject! do |t| stop_descendants.any? { |s| s.left < t.left && s.right > t.right } end
+
+ descendant_tasks.each do |task|
+ task.inherit_version_from(self)
+ task.save if task.changed?
+ end
+ ensure
+ WorkPackage.place_child_update_semaphore
+ end
+ end
+ end
+
+ def validate_parent_work_package_relation
+ if parent && parent_work_package_relationship_spanning_projects?(parent, self)
+ errors.add(:parent,
+ :parent_child_relationship_across_projects,
+ work_package_name: subject,
+ parent_name: parent.subject)
+ end
+ end
+
+ def parent_work_package_relationship_spanning_projects?(parent, child)
+ child.is_task? && parent.in_backlogs_type? && parent.project_id != child.project_id
+ end
+ end
+end
+
+WorkPackage.send(:include, OpenProject::Backlogs::Patches::WorkPackagePatch)
diff --git a/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/version.rb b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/version.rb
new file mode 100644
index 0000000000..f156ab50f5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/open_project/backlogs/version.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module Backlogs
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/lib/openproject-backlogs.rb b/vendored-plugins/openproject-backlogs/lib/openproject-backlogs.rb
new file mode 100644
index 0000000000..816e76410d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/lib/openproject-backlogs.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/backlogs'
diff --git a/vendored-plugins/openproject-backlogs/openproject-backlogs.gemspec b/vendored-plugins/openproject-backlogs/openproject-backlogs.gemspec
new file mode 100644
index 0000000000..81f45a066a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/openproject-backlogs.gemspec
@@ -0,0 +1,24 @@
+$:.push File.expand_path('../lib', __FILE__)
+
+# Maintain your gem's version:
+require 'open_project/backlogs/version'
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = 'openproject-backlogs'
+ s.version = OpenProject::Backlogs::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/plugin-backlogs'
+ s.summary = 'OpenProject Backlogs'
+ s.description = 'This plugin adds features enabling agile teams to work with OpenProject in Scrum projects.'
+ s.files = Dir['{app,config,db,lib,doc}/**/*', 'README.md']
+ s.test_files = Dir['spec/**/*']
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency 'acts_as_silent_list', '~> 2.0.0'
+
+ s.add_dependency 'openproject-pdf_export'
+
+ s.add_development_dependency 'factory_girl_rails', '~> 4.0'
+end
diff --git a/vendored-plugins/openproject-backlogs/package.json b/vendored-plugins/openproject-backlogs/package.json
new file mode 100644
index 0000000000..7c227cc7f9
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "openproject-backlogs",
+ "version": "0.1.0",
+ "main": "frontend/app/openproject-backlogs-app.js",
+ "dependencies": {}
+}
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_package_resource_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_package_resource_spec.rb
new file mode 100644
index 0000000000..9666116462
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_package_resource_spec.rb
@@ -0,0 +1,119 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Work package resource' do
+ include Rack::Test::Methods
+ include Capybara::RSpecMatchers
+
+ let(:current_user) { FactoryGirl.create(:admin) }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:work_package) {
+ FactoryGirl.create(:work_package,
+ project: project,
+ story_points: 8,
+ remaining_hours: 5)
+ }
+ let(:wp_path) { "/api/v3/work_packages/#{work_package.id}" }
+
+ before do
+ allow(Story).to receive(:types).and_return([work_package.type_id])
+ end
+
+ describe '#get' do
+ shared_context 'query work package' do
+ before do
+ allow(User).to receive(:current).and_return(current_user)
+ get wp_path
+ end
+
+ subject { last_response.body }
+ end
+
+ context 'backlogs activated' do
+ include_context 'query work package'
+
+ it { is_expected.to be_json_eql(work_package.story_points.to_json).at_path('storyPoints') }
+
+ it { is_expected.to be_json_eql('PT5H'.to_json).at_path('remainingTime') }
+ end
+
+ context 'backlogs deactivated' do
+ let(:project) {
+ FactoryGirl.create(:project, disable_modules: 'backlogs')
+ }
+
+ include_context 'query work package'
+
+ it { expect(last_response.status).to eql 200 }
+
+ it { is_expected.not_to have_json_path('storyPoints') }
+
+ it { is_expected.not_to have_json_path('remainingTime') }
+ end
+ end
+
+ describe '#patch' do
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ subject { last_response }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ patch wp_path, params.to_json, 'CONTENT_TYPE' => 'application/json'
+ end
+
+ describe 'storyPoints' do
+ let(:params) { valid_params.merge(storyPoints: 12) }
+
+ it { expect(subject.status).to eq(200) }
+ it { expect(subject.body).to be_json_eql(12.to_json).at_path('storyPoints') }
+ end
+
+ describe 'remainingTime' do
+ let(:params) { valid_params.merge(remainingTime: 'PT12H30M') }
+
+ it { expect(subject.status).to eq(200) }
+ it { expect(subject.body).to be_json_eql('PT12H30M'.to_json).at_path('remainingTime') }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_packages/create_contract_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_packages/create_contract_spec.rb
new file mode 100644
index 0000000000..cfc9272ec1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_packages/create_contract_spec.rb
@@ -0,0 +1,94 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::CreateContract do
+ let(:work_package) do
+ FactoryGirl.build(:work_package,
+ project: project)
+ end
+ let(:member) {
+ FactoryGirl.create(:user,
+ member_in_project: project,
+ member_through_role: role)
+ }
+ let (:project) { FactoryGirl.create(:project) }
+ let(:current_user) { member }
+ let(:permissions) {
+ [
+ :view_work_packages,
+ :add_work_packages
+ ]
+ }
+ let(:role) { FactoryGirl.create :role, permissions: permissions }
+ let(:changed_values) { [] }
+
+ subject(:contract) { described_class.new(work_package, current_user) }
+
+ before do
+ allow(work_package).to receive(:changed).and_return(changed_values)
+ end
+
+ describe 'story points' do
+ context 'has not changed' do
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+
+ context 'has changed' do
+ let(:changed_values) { ['story_points'] }
+
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+ end
+
+ describe 'remaining hours' do
+ context 'is no parent' do
+ before do
+ contract.validate
+ end
+
+ context 'has not changed' do
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+
+ context 'has changed' do
+ let(:changed_values) { ['remaining_hours'] }
+
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_packages/form_resource_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_packages/form_resource_spec.rb
new file mode 100644
index 0000000000..3c30da07a5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_packages/form_resource_spec.rb
@@ -0,0 +1,168 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Work package form resource', type: :request do
+ include Rack::Test::Methods
+ include Capybara::RSpecMatchers
+
+ let(:project) { FactoryGirl.create(:project, is_public: false) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ let(:authorized_user) { FactoryGirl.create(:user, member_in_project: project) }
+ let(:unauthorized_user) { FactoryGirl.create(:user) }
+
+ before do
+ allow(Story).to receive(:types).and_return([work_package.type_id])
+ end
+
+ describe '#post' do
+ shared_examples_for 'valid payload' do
+ subject { response.body }
+
+ it { expect(response.status).to eq(200) }
+
+ it { is_expected.to have_json_path('_embedded/payload') }
+
+ it { is_expected.to have_json_path('_embedded/payload/lockVersion') }
+
+ it { is_expected.to have_json_path('_embedded/payload/subject') }
+
+ it_behaves_like 'API V3 formattable', '_embedded/payload/description' do
+ let(:format) { 'textile' }
+ let(:raw) { defined?(raw_value) ? raw_value : work_package.description.to_s }
+ let(:html) {
+ defined?(html_value) ? html_value : ('' + work_package.description.to_s + '
')
+ }
+ end
+ end
+
+ shared_examples_for 'having no errors' do
+ it {
+ expect(subject.body).to be_json_eql({}.to_json).at_path('_embedded/validationErrors')
+ }
+ end
+
+ shared_examples_for 'having an error' do |property|
+ it { expect(subject.body).to have_json_path("_embedded/validationErrors/#{property}") }
+
+ describe 'error body' do
+ let(:error_path) { "_embedded/validationErrors/#{property}" }
+ let(:error_id) { 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation'.to_json }
+
+ let(:error_body) {
+ parse_json(subject.body)['_embedded']['validationErrors'][property]
+ }
+
+ it { expect(subject.body).to have_json_path(error_path) }
+ it {
+ expect(subject.body).to be_json_eql(error_id).at_path("#{error_path}/errorIdentifier")
+ }
+ end
+ end
+
+ let(:post_path) { "/api/v3/work_packages/#{work_package.id}/form" }
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ subject(:response) { last_response }
+
+ shared_context 'post request' do
+ before(:each) do
+ allow(User).to receive(:current).and_return current_user
+ post post_path, (params ? params.to_json : nil), 'CONTENT_TYPE' => 'application/json'
+ end
+ end
+
+ let(:params) {}
+ let(:current_user) { authorized_user }
+
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ describe 'storyPoints' do
+ include_context 'post request'
+
+ context 'valid storyPoints' do
+ let(:params) { valid_params.merge(storyPoints: 42) }
+
+ it_behaves_like 'valid payload'
+
+ it_behaves_like 'having no errors'
+
+ it 'should respond with updated story points' do
+ expect(subject.body).to be_json_eql(42.to_json).at_path('_embedded/payload/storyPoints')
+ end
+ end
+
+ context 'invalid storyPoints' do
+ let(:params) { valid_params.merge(storyPoints: 'two') }
+
+ it_behaves_like 'valid payload'
+
+ it_behaves_like 'having an error', 'storyPoints'
+ end
+ end
+
+ describe 'remainingTime' do
+ include_context 'post request'
+
+ context 'valid remainingTime' do
+ let(:params) { valid_params.merge(remainingTime: 'PT2H45M') }
+
+ it_behaves_like 'valid payload'
+
+ it_behaves_like 'having no errors'
+
+ it 'should respond with updated story points' do
+ expect(subject.body).to be_json_eql('PT2H45M'.to_json)
+ .at_path('_embedded/payload/remainingTime')
+ end
+ end
+
+ context 'invalid remainingTime' do
+ let(:params) { valid_params.merge(remainingTime: 3) }
+
+ it_behaves_like 'format error',
+ I18n.t('api_v3.errors.invalid_format',
+ property: 'remainingTime',
+ expected_format: 'ISO 8601 duration',
+ actual: '3')
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_packages/specific_work_package_schema_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_packages/specific_work_package_schema_spec.rb
new file mode 100644
index 0000000000..a37feb62e1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_packages/specific_work_package_schema_spec.rb
@@ -0,0 +1,63 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
+ let(:project) { FactoryGirl.build(:project) }
+ let(:type) { FactoryGirl.build(:type) }
+ let(:work_package) {
+ FactoryGirl.build(:work_package,
+ project: project,
+ type: type)
+ }
+
+ describe '#remaining_time_writable?' do
+ subject { described_class.new(work_package: work_package) }
+
+ context 'work_package is a leaf' do
+ before do
+ allow(work_package).to receive(:leaf?).and_return(true)
+ end
+
+ it 'is writable' do
+ expect(subject.writable?(:remaining_time)).to eql(true)
+ end
+ end
+
+ context 'work_package is a leaf' do
+ before do
+ allow(work_package).to receive(:leaf?).and_return(false)
+ end
+
+ it 'is not writable' do
+ expect(subject.writable?(:remaining_time)).to eql(false)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_packages/update_contract_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_packages/update_contract_spec.rb
new file mode 100644
index 0000000000..b20755628f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_packages/update_contract_spec.rb
@@ -0,0 +1,113 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::UpdateContract do
+ let(:work_package) do
+ FactoryGirl.create(:work_package,
+ done_ratio: 50,
+ estimated_hours: 6.0,
+ project: project)
+ end
+ let(:member) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let (:project) { FactoryGirl.create(:project) }
+ let(:current_user) { member }
+ let(:permissions) {
+ [
+ :view_work_packages,
+ :view_work_package_watchers,
+ :edit_work_packages,
+ :add_work_package_watchers,
+ :delete_work_package_watchers,
+ :manage_work_package_relations,
+ :add_work_package_notes
+ ]
+ }
+ let(:role) { FactoryGirl.create :role, permissions: permissions }
+ let(:changed_values) { [] }
+
+ subject(:contract) { described_class.new(work_package, current_user) }
+
+ before do
+ allow(work_package).to receive(:changed).and_return(changed_values)
+ end
+
+ describe 'story points' do
+ context 'has not changed' do
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+
+ context 'has changed' do
+ let(:changed_values) { ['story_points'] }
+
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+ end
+
+ describe 'remaining hours' do
+ context 'is no parent' do
+ before do
+ contract.validate
+ end
+
+ context 'has not changed' do
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+
+ context 'has changed' do
+ let(:changed_values) { ['remaining_hours'] }
+
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+ end
+
+ context 'is a parent' do
+ before do
+ child
+ work_package.reload
+ contract.validate
+ end
+ let(:child) do
+ FactoryGirl.create(:work_package, parent_id: work_package.id, project: project)
+ end
+
+ context 'has not changed' do
+ it('is valid') { expect(contract.errors.empty?).to be true }
+ end
+
+ context 'has changed' do
+ let(:changed_values) { ['remaining_hours'] }
+
+ it('is invalid') do
+ expect(contract.errors[:error_readonly]).to match_array(changed_values)
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/api/work_packages/work_package_schema_representer_spec.rb b/vendored-plugins/openproject-backlogs/spec/api/work_packages/work_package_schema_representer_spec.rb
new file mode 100644
index 0000000000..f5cf95f429
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/api/work_packages/work_package_schema_representer_spec.rb
@@ -0,0 +1,132 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
+ let(:custom_field) { FactoryGirl.build(:custom_field) }
+ let(:work_package) { FactoryGirl.build(:work_package) }
+ let(:current_user) {
+ FactoryGirl.build(:user, member_in_project: work_package.project)
+ }
+ let(:schema) {
+ ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema.new(work_package: work_package)
+ }
+ let(:representer) { described_class.create(schema, current_user: current_user) }
+
+ before do
+ allow(schema.project).to receive(:backlogs_enabled?).and_return(true)
+ allow(work_package.type).to receive(:story?).and_return(true)
+ allow(work_package).to receive(:leaf?).and_return(true)
+ end
+
+ describe 'storyPoints' do
+ subject { representer.to_json }
+
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'storyPoints' }
+ let(:type) { 'Integer' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.story_points') }
+ let(:required) { false }
+ let(:writable) { true }
+ end
+
+ context 'backlogs disabled' do
+ before do
+ allow(schema.project).to receive(:backlogs_enabled?).and_return(false)
+ end
+
+ it 'does not show story points' do
+ is_expected.to_not have_json_path('storyPoints')
+ end
+ end
+
+ context 'not a story' do
+ before do
+ allow(schema.type).to receive(:story?).and_return(false)
+ end
+
+ it 'does not show story points' do
+ is_expected.to_not have_json_path('storyPoints')
+ end
+ end
+ end
+
+ describe 'remainingTime' do
+ subject { representer.to_json }
+
+ shared_examples_for 'has schema for remainingTime' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'remainingTime' }
+ let(:type) { 'Duration' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.remaining_hours') }
+ let(:required) { false }
+ let(:writable) { true }
+ end
+ end
+
+ before do
+ allow(schema).to receive(:remaining_time_writable?).and_return(true)
+ end
+
+ it_behaves_like 'has schema for remainingTime'
+
+ context 'backlogs disabled' do
+ before do
+ allow(schema.project).to receive(:backlogs_enabled?).and_return(false)
+ end
+
+ it 'has no schema for remaining time' do
+ is_expected.not_to have_json_path('remainingTime')
+ end
+ end
+
+ context 'not a story' do
+ before do
+ allow(schema.type).to receive(:story?).and_return(false)
+ end
+
+ it_behaves_like 'has schema for remainingTime'
+ end
+
+ context 'remainingTime not writable' do
+ before do
+ allow(schema).to receive(:writable?).and_call_original
+ allow(schema).to receive(:writable?).with(:remaining_time).and_return(false)
+ end
+
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'remainingTime' }
+ let(:type) { 'Duration' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.remaining_hours') }
+ let(:required) { false }
+ let(:writable) { false }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/controllers/versions_controller_spec.rb b/vendored-plugins/openproject-backlogs/spec/controllers/versions_controller_spec.rb
new file mode 100644
index 0000000000..2ade8f0974
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/controllers/versions_controller_spec.rb
@@ -0,0 +1,73 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe VersionsController, type: :controller do
+ before do
+ allow(@controller).to receive(:authorize)
+
+ # Create a version assigned to a project
+ @version = FactoryGirl.create(:version)
+ @oldVersionName = @version.name
+ @newVersionName = 'NewVersionName'
+ # Create another project
+ @project = FactoryGirl.create(:project)
+ # Create params to update version
+ @params = {}
+ @params[:id] = @version.id
+ @params[:version] = { name: @newVersionName }
+ end
+
+ describe 'update' do
+ it 'does not allow to update versions from different projects' do
+ @params[:project_id] = @project.id
+ patch 'update', @params
+ @version.reload
+
+ expect(response).to redirect_to controller: '/projects', action: 'settings', tab: 'versions', id: @project
+ expect(@version.name).to eq(@oldVersionName)
+ end
+
+ it 'allows to update versions from the version project' do
+ @params[:project_id] = @version.project.id
+ patch 'update', @params
+ @version.reload
+
+ expect(response).to redirect_to controller: '/projects', action: 'settings', tab: 'versions', id: @version.project
+ expect(@version.name).to eq(@newVersionName)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/factories/impediment_factory.rb b/vendored-plugins/openproject-backlogs/spec/factories/impediment_factory.rb
new file mode 100644
index 0000000000..2e175264c9
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/factories/impediment_factory.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :impediment do
+ association :type, factory: :type_task
+ subject 'Impeding progress'
+ description 'Unable to print recipes'
+ association :priority, factory: :priority
+ association :author, factory: :user
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/factories/sprint_factory.rb b/vendored-plugins/openproject-backlogs/spec/factories/sprint_factory.rb
new file mode 100644
index 0000000000..b3f0e48586
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/factories/sprint_factory.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :sprint do
+ name 'version'
+ effective_date Date.today + 14.days
+ sharing 'none'
+ status 'open'
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/factories/story_factory.rb b/vendored-plugins/openproject-backlogs/spec/factories/story_factory.rb
new file mode 100644
index 0000000000..deb6d28f36
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/factories/story_factory.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :story do
+ association :priority, factory: :priority
+ sequence(:subject) do |n| "story#{n}" end
+ description 'story story story'
+ association :type, factory: :type_feature
+ association :author, factory: :user
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/factories/task_factory.rb b/vendored-plugins/openproject-backlogs/spec/factories/task_factory.rb
new file mode 100644
index 0000000000..ebc9e1f4b4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/factories/task_factory.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :task do
+ association :type, factory: :type_task
+ subject 'Printing Recipes'
+ description 'Just printing recipes'
+ association :priority, factory: :priority
+ association :author, factory: :user
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/factories/version_setting_factory.rb b/vendored-plugins/openproject-backlogs/spec/factories/version_setting_factory.rb
new file mode 100644
index 0000000000..c446820086
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/factories/version_setting_factory.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :version_setting do
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/features/work_packages/story_points_spec.rb b/vendored-plugins/openproject-backlogs/spec/features/work_packages/story_points_spec.rb
new file mode 100644
index 0000000000..8789ea0571
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/features/work_packages/story_points_spec.rb
@@ -0,0 +1,75 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe 'Work packages having story points', type: :feature, js: true do
+ before do
+ allow(User).to receive(:current).and_return current_user
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return('points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [story_type.id.to_s],
+ 'task_type' => task_type.id.to_s)
+ end
+
+ let(:current_user) { FactoryGirl.create(:admin) }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:status) { FactoryGirl.create :default_status }
+ let(:story_type) { FactoryGirl.create(:type_feature) }
+ let(:task_type) { FactoryGirl.create(:type_feature) }
+
+ describe 'showing the story points on the work package show page' do
+ let(:story_points) { 42 }
+ let(:story_with_sp) {
+ FactoryGirl.create(:story,
+ type: story_type,
+ author: current_user,
+ project: project,
+ status: status,
+ story_points: story_points)
+ }
+
+ it 'should be displayed' do
+ wp_page = Pages::FullWorkPackage.new(story_with_sp)
+
+ wp_page.visit!
+
+ expect(page).to have_selector('#work-package-storyPoints', text: story_points)
+
+ wp_page.ensure_page_loaded
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/helpers/version_settings_helper_spec.rb b/vendored-plugins/openproject-backlogs/spec/helpers/version_settings_helper_spec.rb
new file mode 100644
index 0000000000..354773478a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/helpers/version_settings_helper_spec.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe VersionSettingsHelper, type: :helper do
+ describe '#position_display_options' do
+ before(:each) do
+ @expected_options = [[I18n.t('version_settings_display_option_none'), 1],
+ [I18n.t('version_settings_display_option_left'), 2],
+ [I18n.t('version_settings_display_option_right'), 3]]
+ end
+
+ it { expect(helper.send(:position_display_options)).to eql @expected_options }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/backlog_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/backlog_spec.rb
new file mode 100644
index 0000000000..876a509818
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/backlog_spec.rb
@@ -0,0 +1,62 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Backlog, type: :model do
+ let(:project) { FactoryGirl.build(:project) }
+
+ before(:each) do
+ @feature = FactoryGirl.create(:type_feature)
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [@feature.id.to_s],
+ 'task_type' => '0' })
+ @status = FactoryGirl.create(:status)
+ end
+
+ describe 'Class Methods' do
+ describe '#owner_backlogs' do
+ describe 'WITH one open version defined in the project' do
+ before(:each) do
+ @project = project
+ @work_packages = [FactoryGirl.create(:work_package, subject: 'work_package1', project: @project, type: @feature, status: @status)]
+ @version = FactoryGirl.create(:version, project: project, fixed_issues: @work_packages)
+ @version_settings = @version.version_settings.create(display: VersionSetting::DISPLAY_RIGHT, project: project)
+ end
+
+ it { expect(Backlog.owner_backlogs(@project)[0]).to be_owner_backlog }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/burndown_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/burndown_spec.rb
new file mode 100644
index 0000000000..f335980443
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/burndown_spec.rb
@@ -0,0 +1,216 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Burndown, type: :model do
+ def set_attribute_journalized(story, attribute, value, day)
+ story.reload
+ story.send(attribute, value)
+ story.save!
+ story.current_journal.update_attribute(:created_at, day)
+ end
+
+ let(:user) { @user ||= FactoryGirl.create(:user) }
+ let(:role) { @role ||= FactoryGirl.create(:role) }
+ let(:type_feature) { @type_feature ||= FactoryGirl.create(:type_feature) }
+ let(:type_task) { @type_task ||= FactoryGirl.create(:type_task) }
+ let(:issue_priority) { @issue_priority ||= FactoryGirl.create(:priority, is_default: true) }
+ let(:version) { @version ||= FactoryGirl.create(:version, project: project) }
+ let(:sprint) { @sprint ||= Sprint.find(version.id) }
+
+ let(:project) do
+ unless @project
+ @project = FactoryGirl.build(:project)
+ @project.members = [FactoryGirl.build(:member, principal: user,
+ project: @project,
+ roles: [role])]
+ @project.versions << version
+ end
+ @project
+ end
+
+ let(:issue_open) { @status1 ||= FactoryGirl.create(:status, name: 'status 1', is_default: true) }
+ let(:issue_closed) { @status2 ||= FactoryGirl.create(:status, name: 'status 2', is_closed: true) }
+ let(:issue_resolved) { @status3 ||= FactoryGirl.create(:status, name: 'status 3', is_closed: false) }
+
+ before(:each) do
+ Rails.cache.clear
+
+ allow(User).to receive(:current).and_return(user)
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id.to_s],
+ 'task_type' => type_task.id.to_s })
+
+ project.save!
+
+ [issue_open, issue_closed, issue_resolved].permutation(2).each do |transition|
+ FactoryGirl.create(:workflow,
+ old_status: transition[0],
+ new_status: transition[1],
+ role: role,
+ type_id: type_feature.id)
+ end
+ end
+
+ describe 'Sprint Burndown' do
+ describe 'WITH the today date fixed to April 4th, 2011 and having a 10 (working days) sprint' do
+ before(:each) do
+ allow(Time).to receive(:now).and_return(Time.utc(2011, 'apr', 4, 20, 15, 1))
+ allow(Date).to receive(:today).and_return(Date.civil(2011, 04, 04))
+ end
+
+ describe 'WITH having a version in the future' do
+ before(:each) do
+ version.start_date = Date.today + 1.days
+ version.effective_date = Date.today + 6.days
+ version.save!
+ end
+
+ it 'should generate a burndown' do
+ expect(sprint.burndown(project).series[:story_points]).to be_empty
+ end
+ end
+
+ describe 'WITH having a 10 (working days) sprint and being 5 (working) days into it' do
+ before(:each) do
+ version.start_date = Date.today - 7.days
+ version.effective_date = Date.today + 6.days
+ version.save!
+ end
+
+ describe 'WITH 1 story assigned to the sprint' do
+ before(:each) do
+ @story = FactoryGirl.build(:story, subject: 'Story 1',
+ project: project,
+ fixed_version: version,
+ type: type_feature,
+ status: issue_open,
+ priority: issue_priority,
+ created_at: Date.today - 20.days,
+ updated_at: Date.today - 20.days)
+ end
+
+ describe 'WITH the story having story_point defined on creation' do
+ before(:each) do
+ @story.story_points = 9
+ @story.save!
+ @story.current_journal.update_attribute(:created_at, @story.created_at)
+ end
+
+ describe 'WITH the story being closed and opened again within the sprint duration' do
+ before(:each) do
+ set_attribute_journalized @story, :status_id=, issue_closed.id, Time.now - 6.days
+ set_attribute_journalized @story, :status_id=, issue_open.id, Time.now - 3.days
+
+ @burndown = Burndown.new(sprint, project)
+ end
+
+ it { expect(@burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
+ it { expect(@burndown.story_points.unit).to eql :points }
+ it { expect(@burndown.days).to eql(sprint.days) }
+ it { expect(@burndown.max[:hours]).to eql 0.0 }
+ it { expect(@burndown.max[:points]).to eql 9.0 }
+ it { expect(@burndown.story_points_ideal).to eql [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] }
+ end
+
+ describe "WITH the story marked as resolved and consequently 'done'" do
+ before(:each) do
+ set_attribute_journalized @story, :status_id=, issue_resolved.id, Time.now - 6.days
+ set_attribute_journalized @story, :status_id=, issue_open.id, Time.now - 3.days
+ project.done_statuses << issue_resolved
+ @burndown = Burndown.new(sprint, project)
+ end
+
+ it { expect(@story.done?).to eql false }
+ it { expect(@burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
+ end
+ end
+ end
+
+ describe 'WITH 10 stories assigned to the sprint' do
+ before(:each) do
+ @stories = []
+
+ (0..9).each do |i|
+ @stories[i] = FactoryGirl.create(:story, subject: "Story #{i}",
+ project: project,
+ fixed_version: version,
+ type: type_feature,
+ status: issue_open,
+ priority: issue_priority,
+ created_at: Date.today - (20 - i).days,
+ updated_at: Date.today - (20 - i).days)
+ @stories[i].current_journal.update_attribute(:created_at, @stories[i].created_at)
+ end
+ end
+
+ describe 'WITH each story having story points defined at start' do
+ before(:each) do
+ @stories.each_with_index do |s, _i|
+ set_attribute_journalized s, :story_points=, 10, version.start_date - 3.days
+ end
+ end
+
+ describe 'WITH 5 stories having been reduced to 0 story points, one story per day' do
+ before(:each) do
+ @finished_hours
+ (0..4).each do |i|
+ set_attribute_journalized @stories[i], :story_points=, nil, version.start_date + i.days + 1.hour
+ end
+ end
+
+ describe 'THEN' do
+ before(:each) do
+ @burndown = Burndown.new(sprint, project)
+ end
+
+ it { expect(@burndown.story_points).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 50.0] }
+ it { expect(@burndown.story_points.unit).to eql :points }
+ it { expect(@burndown.days).to eql(sprint.days) }
+ it { expect(@burndown.max[:hours]).to eql 0.0 }
+ it { expect(@burndown.max[:points]).to eql 90.0 }
+ it { expect(@burndown.story_points_ideal).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 0.0] }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/impediment_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/impediment_spec.rb
new file mode 100644
index 0000000000..eeeffa44d4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/impediment_spec.rb
@@ -0,0 +1,356 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Impediment, type: :model do
+ let(:user) { @user ||= FactoryGirl.create(:user) }
+ let(:role) { @role ||= FactoryGirl.create(:role) }
+ let(:type_feature) { @type_feature ||= FactoryGirl.create(:type_feature) }
+ let(:type_task) { @type_task ||= FactoryGirl.create(:type_task) }
+ let(:issue_priority) { @issue_priority ||= FactoryGirl.create(:priority, is_default: true) }
+ let(:task) {
+ FactoryGirl.build(:task, type: type_task,
+ project: project,
+ author: user,
+ priority: issue_priority,
+ status: status1)
+ }
+ let(:feature) {
+ FactoryGirl.build(:work_package, type: type_feature,
+ project: project,
+ author: user,
+ priority: issue_priority,
+ status: status1)
+ }
+ let(:version) { FactoryGirl.create(:version, project: project) }
+
+ let(:project) do
+ unless @project
+ @project = FactoryGirl.build(:project, types: [type_feature, type_task])
+ @project.members = [FactoryGirl.build(:member, principal: user,
+ project: @project,
+ roles: [role])]
+ end
+ @project
+ end
+
+ let(:status1) { @status1 ||= FactoryGirl.create(:status, name: 'status 1', is_default: true) }
+ let(:status2) { @status2 ||= FactoryGirl.create(:status, name: 'status 2') }
+ let(:type_workflow) {
+ @workflow ||= Workflow.create(type_id: type_task.id,
+ old_status: status1,
+ new_status: status2,
+ role: role)
+ }
+ let(:impediment) {
+ FactoryGirl.build(:impediment, author: user,
+ fixed_version: version,
+ assigned_to: user,
+ priority: issue_priority,
+ project: project,
+ type: type_task,
+ status: status1)
+ }
+
+ before(:each) do
+ ActionController::Base.perform_caching = false
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id.to_s],
+ 'task_type' => type_task.id.to_s })
+
+ allow(User).to receive(:current).and_return(user)
+ issue_priority.save
+ status1.save
+ project.save
+ type_workflow.save
+ end
+
+ describe 'class methods' do
+ describe '#create_with_relationships' do
+ before(:each) do
+ @impediment_subject = 'Impediment A'
+ role.permissions = [:create_impediments]
+ role.save
+ end
+
+ shared_examples_for 'impediment creation' do
+ it { expect(@impediment.subject).to eql @impediment_subject }
+ it { expect(@impediment.author).to eql User.current }
+ it { expect(@impediment.project).to eql project }
+ it { expect(@impediment.fixed_version).to eql version }
+ it { expect(@impediment.priority).to eql issue_priority }
+ it { expect(@impediment.status).to eql status1 }
+ it { expect(@impediment.type).to eql type_task }
+ it { expect(@impediment.assigned_to).to eql user }
+ end
+
+ shared_examples_for 'impediment creation with 1 blocking relationship' do
+ it_should_behave_like 'impediment creation'
+ it { expect(@impediment.relations_from.size).to eq(1) }
+ it { expect(@impediment.relations_from[0].to).to eql feature }
+ it { expect(@impediment.relations_from[0].relation_type).to eql Relation::TYPE_BLOCKS }
+ end
+
+ shared_examples_for 'impediment creation with no blocking relationship' do
+ it_should_behave_like 'impediment creation'
+ it { expect(@impediment.relations_from.size).to eq(0) }
+ end
+
+ describe 'WITH a blocking relationship to a story' do
+ describe 'WITH the story having the same version' do
+ before(:each) do
+ feature.fixed_version = version
+ feature.save
+ @impediment = Impediment.create_with_relationships({ subject: @impediment_subject,
+ assigned_to_id: user.id,
+ blocks_ids: feature.id.to_s,
+ status_id: status1.id,
+ fixed_version_id: version.id },
+ project.id)
+ end
+
+ it_should_behave_like 'impediment creation with 1 blocking relationship'
+ it { expect(@impediment).not_to be_new_record }
+ it { expect(@impediment.relations_from[0]).not_to be_new_record }
+ end
+
+ describe 'WITH the story having another version' do
+ before(:each) do
+ feature.fixed_version = FactoryGirl.create(:version, project: project, name: 'another version')
+ feature.save
+ @impediment = Impediment.create_with_relationships({ subject: @impediment_subject,
+ assigned_to_id: user.id,
+ blocks_ids: feature.id.to_s,
+ status_id: status1.id,
+ fixed_version_id: version.id },
+ project.id)
+ end
+
+ it_should_behave_like 'impediment creation with no blocking relationship'
+ it { expect(@impediment).to be_new_record }
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:can_only_contain_work_packages_of_current_sprint, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+
+ describe 'WITH the story being non existent' do
+ before(:each) do
+ @impediment = Impediment.create_with_relationships({ subject: @impediment_subject,
+ assigned_to_id: user.id,
+ blocks_ids: '0',
+ status_id: status1.id,
+ fixed_version_id: version.id },
+ project.id)
+ end
+
+ it_should_behave_like 'impediment creation with no blocking relationship'
+ it { expect(@impediment).to be_new_record }
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:can_only_contain_work_packages_of_current_sprint, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+ end
+
+ describe 'WITHOUT a blocking relationship defined' do
+ before(:each) do
+ @impediment = Impediment.create_with_relationships({ subject: @impediment_subject,
+ assigned_to_id: user.id,
+ blocks_ids: '',
+ status_id: status1.id,
+ fixed_version_id: version.id },
+ project.id)
+ end
+
+ it_should_behave_like 'impediment creation with no blocking relationship'
+ it { expect(@impediment).to be_new_record }
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:must_block_at_least_one_work_package, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+ end
+ end
+
+ describe 'instance methods' do
+ describe '#update_with_relationships' do
+ before(:each) do
+ role.permissions = [:update_impediments]
+ role.save
+
+ feature.fixed_version = version
+ feature.save
+
+ @impediment = impediment
+ @impediment.blocks_ids = feature.id.to_s
+ @impediment.save
+ end
+
+ shared_examples_for 'impediment update' do
+ it { expect(@impediment.author).to eql user }
+ it { expect(@impediment.project).to eql project }
+ it { expect(@impediment.fixed_version).to eql version }
+ it { expect(@impediment.priority).to eql issue_priority }
+ it { expect(@impediment.status).to eql status1 }
+ it { expect(@impediment.type).to eql type_task }
+ it { expect(@impediment.blocks_ids).to eql @blocks.split(/\D+/).map(&:to_i) }
+ end
+
+ shared_examples_for 'impediment update with changed blocking relationship' do
+ it_should_behave_like 'impediment update'
+ it { expect(@impediment.relations_from.size).to eq(1) }
+ it { expect(@impediment.relations_from[0]).not_to be_new_record }
+ it { expect(@impediment.relations_from[0].to).to eql @story }
+ it { expect(@impediment.relations_from[0].relation_type).to eql Relation::TYPE_BLOCKS }
+ end
+
+ shared_examples_for 'impediment update with unchanged blocking relationship' do
+ it_should_behave_like 'impediment update'
+ it { expect(@impediment.relations_from.size).to eq(1) }
+ it { expect(@impediment.relations_from[0]).not_to be_changed }
+ it { expect(@impediment.relations_from[0].to).to eql feature }
+ it { expect(@impediment.relations_from[0].relation_type).to eql Relation::TYPE_BLOCKS }
+ end
+
+ describe 'WHEN changing the blocking relationship to another story' do
+ before(:each) do
+ @story = FactoryGirl.build(:work_package, subject: 'another story',
+ type: type_feature,
+ project: project,
+ author: user,
+ priority: issue_priority,
+ status: status1)
+ end
+
+ describe 'WITH the story having the same version' do
+ before(:each) do
+ @story.fixed_version = version
+ @story.save
+ @blocks = @story.id.to_s
+ @impediment.update_with_relationships({ blocks_ids: @blocks,
+ status_id: status1.id.to_s })
+ end
+
+ it_should_behave_like 'impediment update with changed blocking relationship'
+ it { expect(@impediment).not_to be_changed }
+ end
+
+ describe 'WITH the story having another version' do
+ before(:each) do
+ other_version = FactoryGirl.create(:version, project: project, name: 'another version')
+ # the assignable versions are cached for performance, we thus have to
+ # throw away the cache
+ @story.project = Project.find_by_id(project.id)
+
+ @story.fixed_version = other_version
+ @story.save!
+ @blocks = @story.id.to_s
+ @saved = @impediment.update_with_relationships({ blocks_ids: @blocks,
+ status_id: status1.id.to_s })
+ end
+
+ it_should_behave_like 'impediment update with unchanged blocking relationship'
+ it 'should not be saved successfully' do
+ expect(@saved).to be_falsey
+ end
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:can_only_contain_work_packages_of_current_sprint, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+
+ describe 'WITH the story beeing non existent' do
+ before(:each) do
+ @blocks = '0'
+ @saved = @impediment.update_with_relationships({ blocks_ids: @blocks,
+ status_id: status1.id.to_s })
+ end
+
+ it_should_behave_like 'impediment update with unchanged blocking relationship'
+ it 'should not be saved successfully' do
+ expect(@saved).to be_falsey
+ end
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:can_only_contain_work_packages_of_current_sprint, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+ end
+
+ describe 'WITHOUT a blocking relationship defined' do
+ before(:each) do
+ @blocks = ''
+ @saved = @impediment.update_with_relationships({ blocks_ids: @blocks,
+ status_id: status1.id.to_s })
+ end
+
+ it_should_behave_like 'impediment update with unchanged blocking relationship'
+ it 'should not be saved successfully' do
+ expect(@saved).to be_falsey
+ end
+
+ it { expect(@impediment.errors[:blocks_ids]).to include I18n.t(:must_block_at_least_one_work_package, scope: [:activerecord, :errors, :models, :work_package, :attributes, :blocks_ids]) }
+ end
+ end
+
+ describe 'blocks_ids=/blocks_ids' do
+ describe 'WITH an integer' do
+ it do
+ impediment.blocks_ids = 2
+ expect(impediment.blocks_ids).to eql [2]
+ end
+ end
+
+ describe 'WITH a string' do
+ it do
+ impediment.blocks_ids = '1, 2, 3'
+ expect(impediment.blocks_ids).to eql [1, 2, 3]
+ end
+ end
+
+ describe 'WITH an array' do
+ it do
+ impediment.blocks_ids = [1, 2, 3]
+ expect(impediment.blocks_ids).to eql [1, 2, 3]
+ end
+ end
+
+ describe 'WITH only prior blockers defined' do
+ before(:each) do
+ feature.fixed_version = version
+ feature.save
+ task.fixed_version = version
+ task.save
+
+ impediment.relations_from = [Relation.new(from: impediment, to: feature, relation_type: Relation::TYPE_BLOCKS),
+ Relation.new(from: impediment, to: task, relation_type: Relation::TYPE_BLOCKS)]
+ true
+ end
+
+ it { expect(impediment.blocks_ids).to eql [feature.id, task.id] }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_propagated_down_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_propagated_down_spec.rb
new file mode 100644
index 0000000000..3a22028387
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_propagated_down_spec.rb
@@ -0,0 +1,586 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, "changing a story's fixed_version changes the fixed_version of all it's tasks (and the tasks beyond)", type: :model do
+ let(:type_feature) { FactoryGirl.build(:type_feature) }
+ let(:type_task) { FactoryGirl.build(:type_task) }
+ let(:type_bug) { FactoryGirl.build(:type_bug) }
+ let(:version1) { project.versions.first }
+ let(:version2) { project.versions.last }
+ let(:role) { FactoryGirl.build(:role) }
+ let(:user) { FactoryGirl.build(:user) }
+ let(:issue_priority) { FactoryGirl.build(:priority) }
+ let(:status) { FactoryGirl.build(:status, name: 'status 1', is_default: true) }
+
+ let(:project) do
+ p = FactoryGirl.build(:project, members: [FactoryGirl.build(:member,
+ principal: user,
+ roles: [role])],
+ types: [type_feature, type_task, type_bug])
+
+ p.versions << FactoryGirl.build(:version, name: 'Version1', project: p)
+ p.versions << FactoryGirl.build(:version, name: 'Version2', project: p)
+
+ p
+ end
+
+ let(:story) do
+ story = FactoryGirl.build(:work_package,
+ subject: 'Story',
+ project: project,
+ type: type_feature,
+ fixed_version: version1,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ story
+ end
+
+ let(:story2) do
+ story = FactoryGirl.build(:work_package,
+ subject: 'Story2',
+ project: project,
+ type: type_feature,
+ fixed_version: version1,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ story
+ end
+
+ let(:story3) do
+ story = FactoryGirl.build(:work_package,
+ subject: 'Story3',
+ project: project,
+ type: type_feature,
+ fixed_version: version1,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ story
+ end
+
+ let(:task) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task2',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task3) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task3',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task4) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task4',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task5) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task5',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task6) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task6',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug',
+ type: type_bug,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug2',
+ type: type_bug,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug3) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug3',
+ type: type_bug,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ before(:each) do
+ project.save!
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id],
+ 'task_type' => type_task.id.to_s })
+ end
+
+ def standard_child_layout
+ # Layout is
+ # child
+ # -> task3
+ # -> task4
+ # -> bug3
+ # -> task5
+ # -> story3
+ # -> task6
+ task3.parent_id = child.id
+ task3.save!
+ task4.parent_id = child.id
+ task4.save!
+ bug3.parent_id = child.id
+ bug3.save!
+ story3.parent_id = child.id
+ story3.save!
+
+ task5.parent_id = bug3.id
+ task5.save!
+ task6.parent_id = story3.id
+ task6.save!
+
+ child.reload
+ end
+
+ describe 'WHEN changing fixed_version' do
+ shared_examples_for "changing parent's fixed_version changes child's fixed version" do
+ it "SHOULD change the child's fixed version to the parent's fixed version" do
+ subject.save!
+ child.parent_id = subject.id
+ child.save!
+
+ standard_child_layout
+
+ subject.reload
+
+ subject.fixed_version = version2
+ subject.save!
+
+ # Because of performance, these assertions are all in one it statement
+ expect(child.reload.fixed_version).to eql version2
+ expect(task3.reload.fixed_version).to eql version2
+ expect(task4.reload.fixed_version).to eql version2
+ expect(bug3.reload.fixed_version).to eql version1
+ expect(story3.reload.fixed_version).to eql version1
+ expect(task5.reload.fixed_version).to eql version1
+ expect(task6.reload.fixed_version).to eql version1
+ end
+ end
+
+ shared_examples_for "changing parent's fixed_version does not change child's fixed_version" do
+ it "SHOULD keep the child's version" do
+ subject.save!
+ child.parent_id = subject.id
+ child.save!
+
+ standard_child_layout
+
+ subject.reload
+
+ subject.fixed_version = version2
+ subject.save!
+
+ # Because of performance, these assertions are all in one it statement
+ expect(child.reload.fixed_version).to eql version1
+ expect(task3.reload.fixed_version).to eql version1
+ expect(task4.reload.fixed_version).to eql version1
+ expect(bug3.reload.fixed_version).to eql version1
+ expect(story3.reload.fixed_version).to eql version1
+ expect(task5.reload.fixed_version).to eql version1
+ expect(task6.reload.fixed_version).to eql version1
+ end
+ end
+
+ describe 'WITH backlogs enabled' do
+ before(:each) do
+ project.enabled_module_names += ['backlogs']
+ end
+
+ describe 'WITH a story' do
+ subject { story }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing parent's fixed_version changes child's fixed version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a story as a child' do
+ let(:child) { story2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+
+ describe 'WITH a task (impediment) without a parent' do
+ subject { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing parent's fixed_version changes child's fixed version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+
+ describe 'WITH a non backlogs work_package' do
+ subject { bug }
+
+ describe 'WITH a task as child' do
+ let(:child) { task }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a story as a child' do
+ let(:child) { story }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+ end
+
+ describe 'WITH backlogs disabled' do
+ before(:each) do
+ project.enabled_module_names = project.enabled_module_names.find_all { |n| n != 'backlogs' }
+ end
+
+ describe 'WITH a story' do
+ subject { story }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a story as a child' do
+ let(:child) { story2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+
+ describe 'WITH a task' do
+ before(:each) do
+ bug2.save!
+ task.parent_id = bug2.id # so that it is considered a task
+ task.save!
+ end
+
+ subject { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+
+ describe 'WITH a task (impediment) without a parent' do
+ subject { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+
+ describe 'WITH a non backlogs work_package' do
+ subject { bug }
+
+ describe 'WITH a task as child' do
+ let(:child) { task }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+
+ describe 'WITH a story as a child' do
+ let(:child) { story }
+
+ it_should_behave_like "changing parent's fixed_version does not change child's fixed_version"
+ end
+ end
+ end
+ end
+
+ describe 'WHEN changing the parent_id' do
+ shared_examples_for "changing the child's parent_issue to the parent changes child's fixed version" do
+ it "SHOULD change the child's fixed version to the parent's fixed version" do
+ child.save!
+ standard_child_layout
+
+ parent.fixed_version = version2
+ parent.save!
+ child.parent_id = parent.id
+ child.save!
+
+ # Because of performance, these assertions are all in one it statement
+ expect(child.reload.fixed_version).to eql version2
+ expect(task3.reload.fixed_version).to eql version2
+ expect(task4.reload.fixed_version).to eql version2
+ expect(bug3.reload.fixed_version).to eql version1
+ expect(story3.reload.fixed_version).to eql version1
+ expect(task5.reload.fixed_version).to eql version1
+ expect(task6.reload.fixed_version).to eql version1
+ end
+ end
+
+ shared_examples_for "changing the child's parent to the parent leaves child's fixed version" do
+ it "SHOULD keep the child's version" do
+ child.save!
+ standard_child_layout
+
+ parent.fixed_version = version2
+ parent.save!
+ child.parent_id = parent.id
+ child.save!
+
+ # Because of performance, these assertions are all in one it statement
+ expect(child.reload.fixed_version).to eql version1
+ expect(task3.reload.fixed_version).to eql version1
+ expect(task4.reload.fixed_version).to eql version1
+ expect(bug3.reload.fixed_version).to eql version1
+ expect(story3.reload.fixed_version).to eql version1
+ expect(task5.reload.fixed_version).to eql version1
+ expect(task6.reload.fixed_version).to eql version1
+ end
+ end
+
+ describe 'WITH backogs enabled' do
+ before(:each) do
+ story.project.enabled_module_names += ['backlogs']
+ end
+
+ describe 'WITH a story as parent' do
+ let(:parent) { story }
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing the child's parent_issue to the parent changes child's fixed version"
+ end
+
+ describe 'WITH a non-backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+ end
+
+ describe "WITH a story as parent
+ WITH the story having a non backlogs work_package as parent
+ WITH a task as child" do
+ before do
+ bug2.save!
+ story.parent_id = bug2.id
+ story.save!
+ end
+
+ let(:parent) { story }
+ let(:child) { task2 }
+
+ it_should_behave_like "changing the child's parent_issue to the parent changes child's fixed version"
+ end
+
+ describe 'WITH a task as parent' do
+ before(:each) do
+ story.save!
+ task.parent_id = story.id
+ task.save!
+ story.reload
+ task.reload
+ end
+
+ # Needs to be the story because it is not possible to change a task's
+ # 'fixed_version_id'
+ let(:parent) { story }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing the child's parent_issue to the parent changes child's fixed version"
+ end
+
+ describe 'WITH a non-backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+ end
+
+ describe 'WITH an impediment (task) as parent' do
+ let(:parent) { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing the child's parent_issue to the parent changes child's fixed version"
+ end
+
+ describe 'WITH a non-backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+ end
+
+ describe 'WITH a non-backlogs work_package as parent' do
+ let(:parent) { bug }
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+
+ describe 'WITH a non-backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like "changing the child's parent to the parent leaves child's fixed version"
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_restricted_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_restricted_spec.rb
new file mode 100644
index 0000000000..c91ecdd8c1
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/issue_fixed_version_restricted_spec.rb
@@ -0,0 +1,376 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, "fixed version restricted by an work_package parents (if it's a task)", type: :model do
+ let(:type_feature) { FactoryGirl.build(:type_feature) }
+ let(:type_task) { FactoryGirl.build(:type_task) }
+ let(:type_bug) { FactoryGirl.build(:type_bug) }
+ let(:version1) { project.versions.first }
+ let(:version2) { project.versions.last }
+ let(:role) { FactoryGirl.build(:role) }
+ let(:user) { FactoryGirl.build(:user) }
+ let(:issue_priority) { FactoryGirl.build(:priority) }
+ let(:status) { FactoryGirl.build(:status, name: 'status 1', is_default: true) }
+
+ let(:project) do
+ p = FactoryGirl.build(:project, members: [FactoryGirl.build(:member,
+ principal: user,
+ roles: [role])],
+ types: [type_feature, type_task, type_bug])
+
+ p.versions << FactoryGirl.build(:version, name: 'Version1', project: p)
+ p.versions << FactoryGirl.build(:version, name: 'Version2', project: p)
+
+ p
+ end
+
+ let(:story) do
+ story = FactoryGirl.build(:work_package,
+ subject: 'Story',
+ project: project,
+ type: type_feature,
+ fixed_version: version1,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ story.project.enabled_module_names += ['backlogs']
+ story
+ end
+
+ let(:story2) do
+ story = FactoryGirl.build(:work_package,
+ subject: 'Story2',
+ project: project,
+ type: type_feature,
+ fixed_version: version1,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ story.project.enabled_module_names += ['backlogs']
+ story
+ end
+
+ let(:task) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task2',
+ type: type_task,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug',
+ type: type_bug,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug2',
+ type: type_bug,
+ fixed_version: version1,
+ project: project,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ shared_examples_for 'fixed version beeing inherited from the parent' do
+ before(:each) do
+ parent.save!
+ subject.parent_id = parent.id unless subject.parent_id.present?
+ subject.save!
+ parent.reload
+ end
+
+ describe 'WITHOUT a fixed version and the parent also having no fixed version' do
+ before(:each) do
+ parent.fixed_version = nil
+ parent.save!
+ subject.reload
+ subject.fixed_version = nil
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to be_nil }
+ end
+
+ describe 'WITHOUT a fixed version and the parent having a fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = nil
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+
+ describe 'WITH a fixed version and the parent having a different fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = version2
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+
+ describe 'WITH a fixed version and the parent having the same fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = version1
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+
+ describe 'WITH a fixed version and the parent having no fixed version' do
+ before(:each) do
+ parent.fixed_version = nil
+ parent.save!
+ subject.reload
+ subject.fixed_version = version1
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to be_nil }
+ end
+ end
+
+ shared_examples_for 'fixed version not beeing inherited from the parent' do
+ before(:each) do
+ parent.save!
+ subject.parent_id = parent.id unless subject.parent_id.present?
+ subject.save!
+ parent.reload
+ end
+
+ describe 'WITHOUT a fixed version and the parent also having no fixed version' do
+ before(:each) do
+ parent.fixed_version = nil
+ parent.save!
+ subject.reload
+ subject.fixed_version = nil
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to be_nil }
+ end
+
+ describe 'WITHOUT a fixed version and the parent having a fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = nil
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to be_nil }
+ end
+
+ describe 'WITH a fixed version and the parent having a different fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = version2
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version2 }
+ end
+
+ describe 'WITH a fixed version and the parent having the same fixed version' do
+ before(:each) do
+ parent.fixed_version = version1
+ parent.save!
+ subject.fixed_version = version1
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+
+ describe 'WITH a fixed version and the parent having no fixed version' do
+ before(:each) do
+ parent.fixed_version = nil
+ parent.save!
+ subject.reload
+ subject.fixed_version = version1
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+ end
+
+ shared_examples_for 'fixed version without restriction' do
+ describe 'WITHOUT a fixed version' do
+ before(:each) do
+ subject.fixed_version = nil
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to be_nil }
+ end
+
+ describe 'WITH a fixed version' do
+ before(:each) do
+ subject.fixed_version = version1
+ subject.save!
+ end
+
+ it { expect(subject.reload.fixed_version).to eql version1 }
+ end
+ end
+
+ before(:each) do
+ project.save!
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id],
+ 'task_type' => type_task.id.to_s })
+ end
+
+ describe 'WITH a story' do
+ subject { story }
+
+ describe 'WITHOUT a parent work_package' do
+ it_should_behave_like 'fixed version without restriction'
+ end
+
+ describe "WITH a story as it's parent" do
+ let(:parent) { story2 }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+
+ describe "WITH a non backlogs tracked work_package as it's parent" do
+ let(:parent) { bug }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+ end
+
+ describe 'WITH a task' do
+ subject { task }
+
+ describe 'WITHOUT a parent work_package (would then be an impediment)' do
+ it_should_behave_like 'fixed version without restriction'
+ end
+
+ describe "WITH a task as it's parent" do
+ before(:each) do
+ story.save!
+ task2.parent_id = story.id
+ task2.save!
+ story.reload
+ task.parent_id = task2.id
+ task.save!
+ task2.reload
+ story.reload
+ end
+
+ # It's actually the grandparent but it makes no difference for the test
+ let(:parent) { story }
+
+ it_should_behave_like 'fixed version beeing inherited from the parent'
+ end
+
+ describe "WITH a story as it's parent" do
+ let(:parent) { story }
+
+ it_should_behave_like 'fixed version beeing inherited from the parent'
+ end
+
+ describe "WITH a non backlogs tracked work_package as it's parent" do
+ let(:parent) { bug }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+ end
+
+ describe 'WITH a non backlogs work_package' do
+ subject { bug }
+
+ describe 'WITHOUT a parent work_package' do
+ it_should_behave_like 'fixed version without restriction'
+ end
+
+ describe "WITH a task as it's parent" do
+ let(:parent) { task2 }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+
+ describe "WITH a story as it's parent" do
+ let(:parent) { story }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+
+ describe "WITH a non backlogs tracked work_package as it's parent" do
+ let(:parent) { bug2 }
+
+ it_should_behave_like 'fixed version not beeing inherited from the parent'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/issue_hierarchy_restriction_project_boundaries_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/issue_hierarchy_restriction_project_boundaries_spec.rb
new file mode 100644
index 0000000000..c56a5fa094
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/issue_hierarchy_restriction_project_boundaries_spec.rb
@@ -0,0 +1,421 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+def project_boundaries_spanning_work_package_hierarchy_allowed?
+ work_package = WorkPackage.new
+ work_package.project_id = 1
+ parent_work_package = WorkPackage.new
+ parent_work_package.project_id = 2
+ work_package.parent = parent_work_package
+ work_package.valid?
+ work_package.errors[:parent_id].blank?
+end
+
+describe WorkPackage, 'parent-child relationships between backlogs stories and backlogs tasks are prohibited if they span project boundaries', type: :model do
+ let(:type_feature) { FactoryGirl.build(:type_feature) }
+ let(:type_task) { FactoryGirl.build(:type_task) }
+ let(:type_bug) { FactoryGirl.build(:type_bug) }
+ let(:version1) { project.versions.first }
+ let(:version2) { project.versions.last }
+ let(:role) { FactoryGirl.build(:role) }
+ let(:user) { FactoryGirl.build(:user) }
+ let(:issue_priority) { FactoryGirl.build(:priority) }
+ let(:status) { FactoryGirl.build(:status, name: 'status 1', is_default: true) }
+
+ let(:parent_project) do
+ p = FactoryGirl.build(:project, name: 'parent_project',
+ members: [FactoryGirl.build(:member,
+ principal: user,
+ roles: [role])],
+ types: [type_feature, type_task, type_bug])
+
+ p.versions << FactoryGirl.build(:version, name: 'Version1', project: p)
+ p.versions << FactoryGirl.build(:version, name: 'Version2', project: p)
+
+ p
+ end
+
+ let(:child_project) do
+ p = FactoryGirl.build(:project, name: 'child_project',
+ members: [FactoryGirl.build(:member,
+ principal: user,
+ roles: [role])],
+ types: [type_feature, type_task, type_bug])
+
+ p.versions << FactoryGirl.build(:version, name: 'Version1', project: p)
+ p.versions << FactoryGirl.build(:version, name: 'Version2', project: p)
+
+ p
+ end
+
+ let(:story) {
+ FactoryGirl.build(:work_package,
+ subject: 'Story',
+ type: type_feature,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:story2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Story2',
+ type: type_feature,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task',
+ type: type_task,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:task2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Task2',
+ type: type_task,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug',
+ type: type_bug,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ let(:bug2) {
+ FactoryGirl.build(:work_package,
+ subject: 'Bug2',
+ type: type_bug,
+ status: status,
+ author: user,
+ priority: issue_priority)
+ }
+
+ before do
+ allow(Setting).to receive(:cross_project_work_package_relations).and_return('1')
+ end
+
+ before(:each) do
+ parent_project.save!
+ child_project.save!
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id],
+ 'task_type' => type_task.id.to_s })
+ end
+
+ if project_boundaries_spanning_work_package_hierarchy_allowed?
+
+ describe 'WHEN creating the child' do
+ shared_examples_for 'restricted hierarchy on creation' do
+ before(:each) do
+ parent.project = parent_project
+ parent.save
+
+ child.parent_id = parent.id
+ end
+
+ describe 'WITH the child in a different project' do
+ before(:each) do
+ child.project = child_project
+ end
+
+ it { expect(child).not_to be_valid }
+ end
+
+ describe 'WITH the child in the same project' do
+ before(:each) do
+ child.project = parent_project
+ end
+
+ it { expect(child).to be_valid }
+ end
+ end
+
+ shared_examples_for 'unrestricted hierarchy on creation' do
+ before(:each) do
+ parent.project = parent_project
+ parent.save
+
+ child.parent_id = parent.id
+ end
+
+ describe 'WITH the child in a different project' do
+ before(:each) do
+ child.project = child_project
+ end
+
+ it { expect(child).to be_valid }
+ end
+
+ describe 'WITH the child in the same project' do
+ before(:each) do
+ child.project = parent_project
+ end
+
+ it { expect(child).to be_valid }
+ end
+ end
+
+ describe 'WITH backlogs enabled in both projects' do
+ describe 'WITH a story as parent' do
+ let(:parent) { story }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'restricted hierarchy on creation'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+ end
+
+ describe 'WITH a task as parent (with or without parent does not matter)' do
+ let(:parent) { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'restricted hierarchy on creation'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+ end
+
+ describe 'WITH a non backlogs work_package as parent' do
+ let(:parent) { bug }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy on creation'
+ end
+ end
+ end
+ end
+
+ # This could happen when the project enables backlogs afterwards
+ describe 'WITH an existing child' do
+ shared_examples_for 'restricted hierarchy by enabling backlogs' do
+ before(:each) do
+ parent.project = parent_project
+ parent.save
+
+ child.parent_id = parent.id
+ end
+
+ describe 'WITH the child in a different project' do
+ before(:each) do
+ child_project.enabled_module_names = child_project.enabled_module_names.find_all { |n| n != 'backlogs' }
+ child_project.save!
+ child.project = child_project
+ child_project.reload
+ child.save!
+ child_project.enabled_module_names = child_project.enabled_module_names + ['backlogs']
+ child_project.save!
+ end
+
+ it { expect(child.reload).not_to be_valid }
+ it { expect(parent.reload).not_to be_valid }
+ end
+
+ describe 'WITH the child in the same project' do
+ before(:each) do
+ parent_project.enabled_module_names = parent_project.enabled_module_names.find_all { |n| n != 'backlogs' }
+ parent_project.save!
+ parent_project.reload
+ child.project = parent_project
+ child.save!
+ parent_project.enabled_module_names = parent_project.enabled_module_names + ['backlogs']
+ parent_project.save!
+ end
+
+ it { expect(child.reload).to be_valid }
+ it { expect(parent.reload).to be_valid }
+ end
+ end
+
+ shared_examples_for 'unrestricted hierarchy even when enabling backlogs' do
+ before(:each) do
+ parent.project = parent_project
+ parent.save
+
+ child.parent_id = parent.id
+ end
+
+ describe 'WITH the child in a different project' do
+ before(:each) do
+ child_project.enabled_module_names = child_project.enabled_module_names.find_all { |n| n != 'backlogs' }
+ child_project.save!
+ child.project = child_project
+ child.save!
+ child_project.enabled_module_names = child_project.enabled_module_names + ['backlogs']
+ child_project.save!
+ end
+
+ it { expect(child.reload).to be_valid }
+ it { expect(parent.reload).to be_valid }
+ end
+
+ describe 'WITH the child in the same project' do
+ before(:each) do
+ parent_project.enabled_module_names = parent_project.enabled_module_names.find_all { |n| n != 'backlogs' }
+ parent_project.save!
+ child.project = parent_project
+ child.save!
+ parent_project.enabled_module_names = parent_project.enabled_module_names + ['backlogs']
+ parent_project.save!
+ end
+
+ it { expect(child.reload).to be_valid }
+ it { expect(parent.reload).to be_valid }
+ end
+ end
+
+ describe 'WITH a story as parent' do
+ let(:parent) { story }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'restricted hierarchy by enabling backlogs'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+ end
+
+ describe 'WITH a task as parent' do
+ let(:parent) { task }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'restricted hierarchy by enabling backlogs'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+ end
+
+ describe 'WITH a non-backlogs-work_package as parent' do
+ let(:parent) { bug }
+
+ describe 'WITH a task as child' do
+ let(:child) { task2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+
+ describe 'WITH a non backlogs work_package as child' do
+ let(:child) { bug2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+
+ describe 'WITH a story as child' do
+ let(:child) { story2 }
+
+ it_should_behave_like 'unrestricted hierarchy even when enabling backlogs'
+ end
+ end
+ end
+end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/issue_position_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/issue_position_spec.rb
new file mode 100644
index 0000000000..a79654fc19
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/issue_position_spec.rb
@@ -0,0 +1,418 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, type: :model do
+ describe 'Story positions' do
+ def build_work_package(options)
+ FactoryGirl.build(:work_package, options.reverse_merge(fixed_version_id: sprint_1.id,
+ priority_id: priority.id,
+ project_id: project.id,
+ status_id: status.id,
+ type_id: story_type.id))
+ end
+
+ def create_work_package(options)
+ build_work_package(options).tap(&:save!)
+ end
+
+ let(:status) { FactoryGirl.create(:status) }
+ let(:priority) { FactoryGirl.create(:priority_normal) }
+ let(:project) { FactoryGirl.create(:project) }
+
+ let(:story_type) { FactoryGirl.create(:type, name: 'Story') }
+ let(:epic_type) { FactoryGirl.create(:type, name: 'Epic') }
+ let(:task_type) { FactoryGirl.create(:type, name: 'Task') }
+ let(:other_type) { FactoryGirl.create(:type, name: 'Feedback') }
+
+ let(:sprint_1) { FactoryGirl.create(:version, project_id: project.id, name: 'Sprint 1') }
+ let(:sprint_2) { FactoryGirl.create(:version, project_id: project.id, name: 'Sprint 2') }
+
+ let(:work_package_1) { create_work_package(subject: 'WorkPackage 1', fixed_version_id: sprint_1.id) }
+ let(:work_package_2) { create_work_package(subject: 'WorkPackage 2', fixed_version_id: sprint_1.id) }
+ let(:work_package_3) { create_work_package(subject: 'WorkPackage 3', fixed_version_id: sprint_1.id) }
+ let(:work_package_4) { create_work_package(subject: 'WorkPackage 4', fixed_version_id: sprint_1.id) }
+ let(:work_package_5) { create_work_package(subject: 'WorkPackage 5', fixed_version_id: sprint_1.id) }
+
+ let(:work_package_a) { create_work_package(subject: 'WorkPackage a', fixed_version_id: sprint_2.id) }
+ let(:work_package_b) { create_work_package(subject: 'WorkPackage b', fixed_version_id: sprint_2.id) }
+ let(:work_package_c) { create_work_package(subject: 'WorkPackage c', fixed_version_id: sprint_2.id) }
+
+ let(:feedback_1) {
+ create_work_package(subject: 'Feedback 1', fixed_version_id: sprint_1.id,
+ type_id: other_type.id)
+ }
+
+ let(:task_1) {
+ create_work_package(subject: 'Task 1', fixed_version_id: sprint_1.id,
+ type_id: task_type.id)
+ }
+
+ before do
+ # We had problems while writing these specs, that some elements kept
+ # creaping around between tests. This should be fast enough to not harm
+ # anybody while adding an additional safety net to make sure, that
+ # everything runs in isolation.
+ WorkPackage.delete_all
+ IssuePriority.delete_all
+ Status.delete_all
+ Project.delete_all
+ Type.delete_all
+ Version.delete_all
+
+ # Enable and configure backlogs
+ project.enabled_module_names = project.enabled_module_names + ['backlogs']
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [story_type.id, epic_type.id], 'task_type' => task_type.id })
+
+ # Otherwise the type id's from the previous test are still active
+ WorkPackage.instance_variable_set(:@backlogs_types, nil)
+
+ project.types = [story_type, epic_type, task_type, other_type]
+ sprint_1
+ sprint_2
+
+ # Create and order work_packages
+ work_package_1.move_to_bottom
+ work_package_2.move_to_bottom
+ work_package_3.move_to_bottom
+ work_package_4.move_to_bottom
+ work_package_5.move_to_bottom
+
+ work_package_a.move_to_bottom
+ work_package_b.move_to_bottom
+ work_package_c.move_to_bottom
+ end
+
+ describe '- Creating a work_package in a sprint' do
+ it 'adds it to the bottom of the list' do
+ new_work_package = create_work_package(subject: 'Newest WorkPackage', fixed_version_id: sprint_1.id)
+
+ expect(new_work_package).not_to be_new_record
+ expect(new_work_package).to be_last
+ end
+
+ it 'does not reorder the existing work_packages' do
+ new_work_package = create_work_package(subject: 'Newest WorkPackage', fixed_version_id: sprint_1.id)
+
+ expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
+ end
+ end
+
+ describe '- Removing a work_package from the sprint' do
+ it 'reorders the remaining work_packages' do
+ work_package_2.fixed_version = sprint_2
+ work_package_2.save!
+
+ expect(sprint_1.fixed_issues.order('id')).to eq([work_package_1, work_package_3, work_package_4, work_package_5])
+ expect(sprint_1.fixed_issues.order('id').each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe '- Adding a work_package to a sprint' do
+ it 'adds it to the bottom of the list' do
+ work_package_a.fixed_version = sprint_1
+ work_package_a.save!
+
+ expect(work_package_a).to be_last
+ end
+
+ it 'does not reorder the existing work_packages' do
+ work_package_a.fixed_version = sprint_1
+ work_package_a.save!
+
+ expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
+ end
+ end
+
+ describe '- Deleting a work_package in a sprint' do
+ it 'reorders the existing work_packages' do
+ work_package_3.destroy
+
+ expect([work_package_1, work_package_2, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe '- Changing the type' do
+ describe 'by moving a story to another story type' do
+ it 'keeps all positions in the sprint in tact' do
+ work_package_3.type = epic_type
+ work_package_3.save!
+
+ expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
+ end
+ end
+
+ describe 'by moving a story to a non-backlogs type' do
+ it 'removes it from any list' do
+ work_package_3.type = other_type
+ work_package_3.save!
+
+ expect(work_package_3).not_to be_in_list
+ end
+
+ it 'reorders the remaining stories' do
+ work_package_3.type = other_type
+ work_package_3.save!
+
+ expect([work_package_1, work_package_2, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe 'by moving a story to the task type' do
+ it 'removes it from any list' do
+ work_package_3.type = task_type
+ work_package_3.save!
+
+ expect(work_package_3).not_to be_in_list
+ end
+
+ it 'reorders the remaining stories' do
+ work_package_3.type = task_type
+ work_package_3.save!
+
+ expect([work_package_1, work_package_2, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe 'by moving a task to the story type' do
+ it 'adds it to the bottom of the list' do
+ task_1.type = story_type
+ task_1.save!
+
+ expect(task_1).to be_last
+ end
+
+ it 'does not reorder the existing stories' do
+ task_1.type = story_type
+ task_1.save!
+
+ expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5, task_1].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5, 6])
+ end
+ end
+
+ describe 'by moving a non-backlogs work_package to a story type' do
+ it 'adds it to the bottom of the list' do
+ feedback_1.type = story_type
+ feedback_1.save!
+
+ expect(feedback_1).to be_last
+ end
+
+ it 'does not reorder the existing stories' do
+ feedback_1.type = story_type
+ feedback_1.save!
+
+ expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5, feedback_1].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5, 6])
+ end
+ end
+ end
+
+ describe '- Moving work_packages between projects' do
+ # N.B.: You cannot move a ticket to another project and change the
+ # 'fixed_version' at the same time. On the other hand, OpenProject tries
+ # to keep the 'fixed_version' if possible (e.g. within project
+ # hierarchies with shared versions)
+
+ let(:project_wo_backlogs) { FactoryGirl.create(:project) }
+ let(:sub_project_wo_backlogs) { FactoryGirl.create(:project) }
+
+ let(:shared_sprint) {
+ FactoryGirl.create(:version,
+ project_id: project.id,
+ name: 'Shared Sprint',
+ sharing: 'descendants')
+ }
+
+ let(:version_go_live) {
+ FactoryGirl.create(:version,
+ project_id: project_wo_backlogs.id,
+ name: 'Go-Live')
+ }
+ let(:admin) { FactoryGirl.create(:admin) }
+
+ def move_to_project(work_package, project)
+ service = MoveWorkPackageService.new(work_package, admin)
+
+ service.call(project)
+ end
+
+ before do
+ project_wo_backlogs.enabled_module_names = project_wo_backlogs.enabled_module_names - ['backlogs']
+ sub_project_wo_backlogs.enabled_module_names = sub_project_wo_backlogs.enabled_module_names - ['backlogs']
+
+ project_wo_backlogs.types = [story_type, task_type, other_type]
+ sub_project_wo_backlogs.types = [story_type, task_type, other_type]
+
+ sub_project_wo_backlogs.move_to_child_of(project)
+
+ shared_sprint
+ version_go_live
+ end
+
+ describe '- Moving an work_package from a project without backlogs to a backlogs_enabled project' do
+ describe 'if the fixed_version may not be kept' do
+ let(:work_package_i) {
+ create_work_package(subject: 'WorkPackage I',
+ fixed_version_id: version_go_live.id,
+ project_id: project_wo_backlogs.id)
+ }
+ before do
+ work_package_i
+ end
+
+ it 'sets the fixed_version_id to nil' do
+ result = move_to_project(work_package_i, project)
+
+ expect(result).to be_truthy
+
+ expect(work_package_i.fixed_version).to be_nil
+ end
+
+ it 'removes it from any list' do
+ result = move_to_project(work_package_i, project)
+
+ expect(result).to be_truthy
+
+ expect(work_package_i).not_to be_in_list
+ end
+ end
+
+ describe 'if the fixed_version may be kept' do
+ let(:work_package_i) {
+ create_work_package(subject: 'WorkPackage I',
+ fixed_version_id: shared_sprint.id,
+ project_id: sub_project_wo_backlogs.id)
+ }
+
+ before do
+ work_package_i
+ end
+
+ it 'keeps the fixed_version_id' do
+ result = move_to_project(work_package_i, project)
+
+ expect(result).to be_truthy
+
+ expect(work_package_i.fixed_version).to eq(shared_sprint)
+ end
+
+ it 'adds it to the bottom of the list' do
+ result = move_to_project(work_package_i, project)
+
+ expect(result).to be_truthy
+
+ expect(work_package_i).to be_first
+ end
+ end
+ end
+
+ describe '- Moving an work_package away from backlogs_enabled project to a project without backlogs' do
+ describe 'if the fixed_version may not be kept' do
+ it 'sets the fixed_version_id to nil' do
+ result = move_to_project(work_package_3, project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect(work_package_3.fixed_version).to be_nil
+ end
+
+ it 'removes it from any list' do
+ result = move_to_project(work_package_3, sub_project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect(work_package_3).not_to be_in_list
+ end
+
+ it 'reorders the remaining work_packages' do
+ result = move_to_project(work_package_3, sub_project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect([work_package_1, work_package_2, work_package_4, work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe 'if the fixed_version may be kept' do
+ let(:work_package_i) {
+ create_work_package(subject: 'WorkPackage I',
+ fixed_version_id: shared_sprint.id)
+ }
+ let(:work_package_ii) {
+ create_work_package(subject: 'WorkPackage II',
+ fixed_version_id: shared_sprint.id)
+ }
+ let(:work_package_iii) {
+ create_work_package(subject: 'WorkPackage III',
+ fixed_version_id: shared_sprint.id)
+ }
+
+ before do
+ work_package_i.move_to_bottom
+ work_package_ii.move_to_bottom
+ work_package_iii.move_to_bottom
+
+ expect([work_package_i, work_package_ii, work_package_iii].map(&:position)).to eq([1, 2, 3])
+ end
+
+ it 'keeps the fixed_version_id' do
+ result = move_to_project(work_package_ii, sub_project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect(work_package_ii.fixed_version).to eq(shared_sprint)
+ end
+
+ it 'removes it from any list' do
+ result = move_to_project(work_package_ii, sub_project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect(work_package_ii).not_to be_in_list
+ end
+
+ it 'reorders the remaining work_packages' do
+ result = move_to_project(work_package_ii, sub_project_wo_backlogs)
+
+ expect(result).to be_truthy
+
+ expect([work_package_i, work_package_iii].each(&:reload).map(&:position)).to eq([1, 2])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/issue_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/issue_spec.rb
new file mode 100644
index 0000000000..6d1195cc1b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/issue_spec.rb
@@ -0,0 +1,189 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, type: :model do
+ describe 'behavior for #3200' do
+ let(:empty_work_package) { WorkPackage.new }
+ let(:admin) { FactoryGirl.create(:admin) }
+
+ def move_to_project_without_transaction(work_package, project)
+ service = MoveWorkPackageService.new(work_package, admin)
+
+ service.call(project, nil, no_transaction: true)
+ end
+
+ it do
+ expect(move_to_project_without_transaction(empty_work_package, nil)).to be_falsey
+ end
+ end
+
+ describe 'validations' do
+ let(:work_package) do
+ FactoryGirl.build(:work_package)
+ end
+
+ describe 'story points' do
+ before(:each) do
+ work_package.project.enabled_module_names += ['backlogs']
+ end
+
+ it 'allows empty values' do
+ expect(work_package.story_points).to be_nil
+ expect(work_package).to be_valid
+ end
+
+ it 'allows values greater than or equal to 0' do
+ work_package.story_points = '0'
+ expect(work_package).to be_valid
+
+ work_package.story_points = '1'
+ expect(work_package).to be_valid
+ end
+
+ it 'allows values less than 10.000' do
+ work_package.story_points = '9999'
+ expect(work_package).to be_valid
+ end
+
+ it 'disallows negative values' do
+ work_package.story_points = '-1'
+ expect(work_package).not_to be_valid
+ end
+
+ it 'disallows greater or equal than 10.000' do
+ work_package.story_points = '10000'
+ expect(work_package).not_to be_valid
+
+ work_package.story_points = '10001'
+ expect(work_package).not_to be_valid
+ end
+
+ it 'disallows string values, that are not numbers' do
+ work_package.story_points = 'abc'
+ expect(work_package).not_to be_valid
+ end
+
+ it 'disallows non-integers' do
+ work_package.story_points = '1.3'
+ expect(work_package).not_to be_valid
+ end
+ end
+
+ describe 'remaining hours' do
+ it 'allows empty values' do
+ expect(work_package.remaining_hours).to be_nil
+ expect(work_package).to be_valid
+ end
+
+ it 'allows values greater than or equal to 0' do
+ work_package.remaining_hours = '0'
+ expect(work_package).to be_valid
+
+ work_package.remaining_hours = '1'
+ expect(work_package).to be_valid
+ end
+
+ it 'disallows negative values' do
+ work_package.remaining_hours = '-1'
+ expect(work_package).not_to be_valid
+ end
+
+ it 'disallows string values, that are not numbers' do
+ work_package.remaining_hours = 'abc'
+ expect(work_package).not_to be_valid
+ end
+
+ it 'allows non-integers' do
+ work_package.remaining_hours = '1.3'
+ expect(work_package).to be_valid
+ end
+ end
+ end
+
+ describe 'definition of done' do
+ before(:each) do
+ @status_resolved = FactoryGirl.build(:status, name: 'Resolved', is_default: false)
+ @status_open = FactoryGirl.build(:status, name: 'Open', is_default: true)
+ @project = FactoryGirl.build(:project)
+ @project.done_statuses = [@status_resolved]
+ @project.types = [FactoryGirl.build(:type_feature)]
+
+ @work_package = FactoryGirl.build(:work_package, project: @project,
+ status: @status_open,
+ type: FactoryGirl.build(:type_feature))
+ end
+
+ it 'should not be done when having the initial status "open"' do
+ expect(@work_package.done?).to be_falsey
+ end
+
+ it 'should be done when having the status "resolved"' do
+ @work_package.status = @status_resolved
+ expect(@work_package.done?).to be_truthy
+ end
+
+ it 'should not be done when removing done status from "resolved"' do
+ @work_package.status = @status_resolved
+ @project.done_statuses = Array.new
+ expect(@work_package.done?).to be_falsey
+ end
+ end
+
+ describe 'backlogs_enabled?' do
+ let(:project) { FactoryGirl.build(:project) }
+ let(:work_package) { FactoryGirl.build(:work_package) }
+
+ it 'should be false without a project' do
+ work_package.project = nil
+ expect(work_package).not_to be_backlogs_enabled
+ end
+
+ it 'should be true with a project having the backlogs module' do
+ project.enabled_module_names = project.enabled_module_names + ['backlogs']
+ work_package.project = project
+
+ expect(work_package).to be_backlogs_enabled
+ end
+
+ it 'should be false with a project not having the backlogs module' do
+ work_package.project = project
+ work_package.project.enabled_module_names = nil
+
+ expect(work_package).not_to be_backlogs_enabled
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/sprint_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/sprint_spec.rb
new file mode 100644
index 0000000000..fda2edb411
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/sprint_spec.rb
@@ -0,0 +1,153 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Sprint, type: :model do
+ let(:sprint) { FactoryGirl.build(:sprint) }
+ let(:project) { FactoryGirl.build(:project) }
+
+ describe 'Class Methods' do
+ describe '#displayed_left' do
+ describe 'WITH display set to left' do
+ before(:each) do
+ sprint.version_settings = [FactoryGirl.build(:version_setting, project: project,
+ display: VersionSetting::DISPLAY_LEFT)]
+ sprint.project = project
+ sprint.save!
+ end
+
+ it {
+ expect(Sprint.displayed_left(project)).to match_array [sprint]
+ }
+ end
+
+ describe 'WITH a version setting defined for another project' do
+ before(:each) do
+ another_project = FactoryGirl.build(:project, name: 'another project',
+ identifier: 'another project')
+
+ sprint.version_settings = [FactoryGirl.build(:version_setting, project: another_project,
+ display: VersionSetting::DISPLAY_RIGHT)]
+ sprint.project = project
+ sprint.save
+ end
+
+ it { expect(Sprint.displayed_left(project)).to match_array [sprint] }
+ end
+
+ describe 'WITH no version setting defined' do
+ before(:each) do
+ sprint.project = project
+ sprint.save!
+ end
+
+ it { expect(Sprint.displayed_left(project)).to match_array [sprint] }
+ end
+ end
+
+ describe '#displayed_right' do
+ before(:each) do
+ sprint.version_settings = [FactoryGirl.build(:version_setting, project: project, display: VersionSetting::DISPLAY_RIGHT)]
+ sprint.project = project
+ sprint.save!
+ end
+
+ it { expect(Sprint.displayed_right(project)).to match_array [sprint] }
+ end
+
+ describe '#order_by_date' do
+ before(:each) do
+ @sprint1 = FactoryGirl.create(:sprint, name: 'sprint1', project: project, start_date: Date.today + 2.days)
+ @sprint2 = FactoryGirl.create(:sprint, name: 'sprint2', project: project, start_date: Date.today + 1.day, effective_date: Date.today + 3.days)
+ @sprint3 = FactoryGirl.create(:sprint, name: 'sprint3', project: project, start_date: Date.today + 1.day, effective_date: Date.today + 2.days)
+ end
+
+ it { expect(Sprint.order_by_date[0]).to eql @sprint3 }
+ it { expect(Sprint.order_by_date[1]).to eql @sprint2 }
+ it { expect(Sprint.order_by_date[2]).to eql @sprint1 }
+ end
+
+ describe '#apply_to' do
+ before(:each) do
+ project.save
+ @other_project = FactoryGirl.create(:project)
+ end
+
+ describe 'WITH the version beeing shared system wide' do
+ before(:each) do
+ @version = FactoryGirl.create(:sprint, name: 'systemwide', project: @other_project, sharing: 'system')
+ end
+
+ it { expect(Sprint.apply_to(project).size).to eq(1) }
+ it { expect(Sprint.apply_to(project)[0]).to eql(@version) }
+ end
+
+ describe 'WITH the version beeing shared from a parent project' do
+ before(:each) do
+ project.set_parent!(@other_project)
+ @version = FactoryGirl.create(:sprint, name: 'descended', project: @other_project, sharing: 'descendants')
+ end
+
+ it { expect(Sprint.apply_to(project).size).to eq(1) }
+ it { expect(Sprint.apply_to(project)[0]).to eql(@version) }
+ end
+
+ describe 'WITH the version beeing shared within the tree' do
+ before(:each) do
+ @parent_project = FactoryGirl.create(:project)
+ # Setting the parent has to be in this order, don't know why yet
+ @other_project.set_parent!(@parent_project)
+ project.set_parent!(@parent_project)
+ @version = FactoryGirl.create(:sprint, name: 'treed', project: @other_project, sharing: 'tree')
+ end
+
+ it { expect(Sprint.apply_to(project).size).to eq(1) }
+ it { expect(Sprint.apply_to(project)[0]).to eql(@version) }
+ end
+
+ describe 'WITH the version beeing shared within the tree' do
+ before(:each) do
+ @descendant_project = FactoryGirl.create(:project)
+ @descendant_project.set_parent!(project)
+ @version = FactoryGirl.create(:sprint, name: 'hierar', project: @descendant_project, sharing: 'hierarchy')
+ end
+
+ it { expect(Sprint.apply_to(project).size).to eq(1) }
+ it { expect(Sprint.apply_to(project)[0]).to eql(@version) }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/story_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/story_spec.rb
new file mode 100644
index 0000000000..79652d3b9d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/story_spec.rb
@@ -0,0 +1,236 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Story, type: :model do
+ let(:user) { @user ||= FactoryGirl.create(:user) }
+ let(:role) { @role ||= FactoryGirl.create(:role) }
+ let(:status1) { @status1 ||= FactoryGirl.create(:status, name: 'status 1', is_default: true) }
+ let(:type_feature) { @type_feature ||= FactoryGirl.create(:type_feature) }
+ let(:version) { @version ||= FactoryGirl.create(:version, project: project) }
+ let(:version2) { FactoryGirl.create(:version, project: project) }
+ let(:sprint) { @sprint ||= FactoryGirl.create(:sprint, project: project) }
+ let(:issue_priority) { @issue_priority ||= FactoryGirl.create(:priority) }
+ let(:task_type) { FactoryGirl.create(:type_task) }
+ let(:task) {
+ FactoryGirl.create(:story, fixed_version: version,
+ project: project,
+ status: status1,
+ type: task_type,
+ priority: issue_priority)
+ }
+ let(:story1) {
+ FactoryGirl.create(:story, fixed_version: version,
+ project: project,
+ status: status1,
+ type: type_feature,
+ priority: issue_priority)
+ }
+
+ let(:story2) {
+ FactoryGirl.create(:story, fixed_version: version,
+ project: project,
+ status: status1,
+ type: type_feature,
+ priority: issue_priority)
+ }
+
+ let(:project) do
+ unless @project
+ @project = FactoryGirl.build(:project)
+ @project.members = [FactoryGirl.build(:member, principal: user,
+ project: @project,
+ roles: [role])]
+ end
+ @project
+ end
+
+ before(:each) do
+ ActionController::Base.perform_caching = false
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'points_burn_direction' => 'down',
+ 'wiki_template' => '',
+ 'card_spec' => 'Sattleford VM-5040',
+ 'story_types' => [type_feature.id.to_s],
+ 'task_type' => task_type.id.to_s })
+ project.types << task_type
+ end
+
+ describe 'Class methods' do
+ describe '#backlogs' do
+ describe "WITH one sprint
+ WITH the sprint having 1 story" do
+ before(:each) do
+ story1
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1]) }
+ end
+
+ describe "WITH two sprints
+ WITH two stories
+ WITH one story per sprint
+ WITH querying for the two sprints" do
+ before do
+ version2
+ story1
+ story2.fixed_version_id = version2.id
+ story2.save!
+ end
+
+ it { expect(Story.backlogs(project, [version.id, version2.id])[version.id]).to match_array([story1]) }
+ it { expect(Story.backlogs(project, [version.id, version2.id])[version2.id]).to match_array([story2]) }
+ end
+
+ describe "WITH two sprints
+ WITH two stories
+ WITH one story per sprint
+ WITH querying one sprints" do
+ before do
+ version2
+ story1
+
+ story2.fixed_version_id = version2.id
+ story2.save!
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1]) }
+ it { expect(Story.backlogs(project, [version.id])[version2.id]).to be_empty }
+ end
+
+ describe "WITH two sprints
+ WITH two stories
+ WITH one story per sprint
+ WITH querying for the two sprints
+ WITH one sprint beeing in another project" do
+ before do
+ story1
+
+ other_project = FactoryGirl.create(:project)
+ version2.project_id = other_project.id
+ story2.fixed_version_id = version2.id
+ story2.project = other_project
+ story2.save!
+ end
+
+ it { expect(Story.backlogs(project, [version.id, version2.id])[version.id]).to match_array([story1]) }
+ it { expect(Story.backlogs(project, [version.id, version2.id])[version2.id]).to be_empty }
+ end
+
+ describe "WITH one sprint
+ WITH the sprint having one story in this project and one story in another project" do
+ before(:each) do
+ version.sharing = 'system'
+ version.save!
+
+ another_project = FactoryGirl.create(:project)
+
+ story1
+ story2.project = another_project
+ story2.save!
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1]) }
+ end
+
+ describe "WITH one sprint
+ WITH the sprint having two storys
+ WITH one beeing the child of the other" do
+ before(:each) do
+ story1.parent_id = story2.id
+
+ story1.save
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1, story2]) }
+ end
+
+ describe "WITH one sprint
+ WITH the sprint having one story
+ WITH the story having a child task" do
+ before(:each) do
+ task.parent_id = story1.id
+
+ task.save
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1]) }
+ end
+
+ describe "WITH one sprint
+ WITH the sprint having one story and one task
+ WITH the two having no connection" do
+ before(:each) do
+ task
+ story1
+ end
+
+ it { expect(Story.backlogs(project, [version.id])[version.id]).to match_array([story1]) }
+ end
+ end
+ end
+
+ describe 'journals created after adding a subtask to a story' do
+ before(:each) do
+ @current = FactoryGirl.create(:user, login: 'user1', mail: 'user1@users.com')
+ allow(User).to receive(:current).and_return(@current)
+
+ @story = FactoryGirl.create(:story, fixed_version: version,
+ project: project,
+ status: status1,
+ type: type_feature,
+ priority: issue_priority)
+ @story.project.enabled_module_names += ['backlogs']
+
+ @work_package ||= FactoryGirl.create(:work_package, project: project, status: status1, type: type_feature, author: @current)
+ end
+
+ it 'should create a journal when adding a subtask which has remaining hours set' do
+ @work_package.remaining_hours = 15.0
+ @work_package.parent_id = @story.id
+ @work_package.save!
+
+ expect(@story.journals.last.changed_data[:remaining_hours]).to eq([nil, 15])
+ end
+
+ it 'should not create an empty journal when adding a subtask without remaining hours set' do
+ @work_package.parent_id = @story.id
+ @work_package.save!
+
+ expect(@story.journals.last.changed_data[:remaining_hours]).to be_nil
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/user_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/user_spec.rb
new file mode 100644
index 0000000000..a1152e0760
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/user_spec.rb
@@ -0,0 +1,63 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe User, type: :model do
+ describe 'backlogs_preference' do
+ describe 'task_color' do
+ it 'reads from and writes to a user preference' do
+ u = FactoryGirl.create(:user)
+ u.backlogs_preference(:task_color, '#FFCC33')
+
+ expect(u.backlogs_preference(:task_color)).to eq('#FFCC33')
+ u.reload
+ expect(u.backlogs_preference(:task_color)).to eq('#FFCC33')
+ end
+
+ it 'computes a random color and persists it, when none is set' do
+ u = FactoryGirl.create(:user)
+ u.backlogs_preference(:task_color, nil)
+ u.save!
+
+ generated_task_color = u.backlogs_preference(:task_color)
+ expect(generated_task_color).to match(/^#[0-9A-F]{6}$/)
+ u.save!
+ u.reload
+ expect(u.backlogs_preference(:task_color)).to eq(generated_task_color)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/version_setting_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/version_setting_spec.rb
new file mode 100644
index 0000000000..409d769b91
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/version_setting_spec.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe VersionSetting, type: :model do
+ let(:version_setting) { FactoryGirl.build(:version_setting) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:version) }
+ it { expect(VersionSetting.column_names).to include('display') }
+
+ describe 'Instance Methods' do
+ describe 'WITH display set to left' do
+ before(:each) do
+ version_setting.display_left!
+ end
+
+ it { expect(version_setting.display_left?).to be_truthy }
+ end
+
+ describe 'WITH display set to right' do
+ before(:each) do
+ version_setting.display_right!
+ end
+
+ it { expect(version_setting.display_right?).to be_truthy }
+ end
+
+ describe 'WITH display set to none' do
+ before(:each) do
+ version_setting.display_none!
+ end
+
+ it { expect(version_setting.display_none?).to be_truthy }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/version_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/version_spec.rb
new file mode 100644
index 0000000000..ccfc0e5254
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/version_spec.rb
@@ -0,0 +1,167 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Version, type: :model do
+ it { is_expected.to have_many :version_settings }
+
+ describe 'rebuild positions' do
+ def build_work_package(options = {})
+ FactoryGirl.build(:work_package, options.reverse_merge(fixed_version_id: version.id,
+ priority_id: priority.id,
+ project_id: project.id,
+ status_id: status.id))
+ end
+
+ def create_work_package(options = {})
+ build_work_package(options).tap(&:save!)
+ end
+
+ let(:status) { FactoryGirl.create(:status) }
+ let(:priority) { FactoryGirl.create(:priority_normal) }
+ let(:project) { FactoryGirl.create(:project, name: 'Project 1', types: [epic_type, story_type, task_type, other_type]) }
+
+ let(:epic_type) { FactoryGirl.create(:type, name: 'Epic') }
+ let(:story_type) { FactoryGirl.create(:type, name: 'Story') }
+ let(:task_type) { FactoryGirl.create(:type, name: 'Task') }
+ let(:other_type) { FactoryGirl.create(:type, name: 'Other') }
+
+ let(:version) { FactoryGirl.create(:version, project_id: project.id, name: 'Version') }
+
+ let(:admin) { FactoryGirl.create(:admin) }
+
+ def move_to_project(work_package, project)
+ service = MoveWorkPackageService.new(work_package, admin)
+
+ service.call(project)
+ end
+
+ before do
+ # We had problems while writing these specs, that some elements kept
+ # creaping around between tests. This should be fast enough to not harm
+ # anybody while adding an additional safety net to make sure, that
+ # everything runs in isolation.
+ WorkPackage.delete_all
+ IssuePriority.delete_all
+ Status.delete_all
+ Project.delete_all
+ Type.delete_all
+ Version.delete_all
+
+ # Enable and configure backlogs
+ project.enabled_module_names = project.enabled_module_names + ['backlogs']
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [epic_type.id, story_type.id], 'task_type' => task_type.id })
+
+ # Otherwise the type id's from the previous test are still active
+ WorkPackage.instance_variable_set(:@backlogs_types, nil)
+
+ project.types = [epic_type, story_type, task_type, other_type]
+ version
+ end
+
+ it 'moves an work_package to a project where backlogs is disabled while using versions' do
+ project2 = FactoryGirl.create(:project, name: 'Project 2', types: [epic_type, story_type, task_type, other_type])
+ project2.enabled_module_names = project2.enabled_module_names - ['backlogs']
+ project2.save!
+ project2.reload
+
+ work_package1 = FactoryGirl.create(:work_package, type_id: task_type.id, status_id: status.id, project_id: project.id)
+ work_package2 = FactoryGirl.create(:work_package, parent_id: work_package1.id, type_id: task_type.id, status_id: status.id, project_id: project.id)
+ work_package3 = FactoryGirl.create(:work_package, parent_id: work_package2.id, type_id: task_type.id, status_id: status.id, project_id: project.id)
+
+ work_package1.reload
+ work_package1.fixed_version_id = version.id
+ work_package1.save!
+
+ work_package1.reload
+ work_package2.reload
+ work_package3.reload
+
+ move_to_project(work_package3, project2)
+
+ work_package1.reload
+ work_package2.reload
+ work_package3.reload
+
+ move_to_project(work_package2, project2)
+
+ work_package1.reload
+ work_package2.reload
+ work_package3.reload
+
+ expect(work_package3.project).to eq(project2)
+ expect(work_package2.project).to eq(project2)
+ expect(work_package1.project).to eq(project)
+
+ expect(work_package3.fixed_version_id).to be_nil
+ expect(work_package2.fixed_version_id).to be_nil
+ expect(work_package1.fixed_version_id).to eq(version.id)
+ end
+
+ it 'rebuilds postions' do
+ e1 = create_work_package(type_id: epic_type.id)
+ s2 = create_work_package(type_id: story_type.id)
+ s3 = create_work_package(type_id: story_type.id)
+ s4 = create_work_package(type_id: story_type.id)
+ s5 = create_work_package(type_id: story_type.id)
+ t3 = create_work_package(type_id: task_type.id)
+ o9 = create_work_package(type_id: other_type.id)
+
+ [e1, s2, s3, s4, s5].each(&:move_to_bottom)
+
+ # Messing around with positions
+ s3.send :assume_not_in_list
+ s4.send :assume_not_in_list
+
+ t3.send(:update_attribute_silently, :position, 3)
+ o9.send(:update_attribute_silently, :position, 9)
+
+ version.rebuild_positions(project)
+
+ work_packages = version
+ .fixed_issues
+ .where(project_id: project)
+ .order('COALESCE(position, 0) ASC, id ASC')
+
+ expect(work_packages.map(&:position)).to eq([nil, nil, 1, 2, 3, 4, 5])
+ expect(work_packages.map(&:subject)).to eq([t3, o9, e1, s2, s5, s3, s4].map(&:subject))
+
+ # Makes sure, that all work_package subjects are uniq, so that the above
+ # assertion works as expected
+ expect(work_packages.map(&:subject).uniq.size).to eq(7)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/models/work_package_spec.rb b/vendored-plugins/openproject-backlogs/spec/models/work_package_spec.rb
new file mode 100644
index 0000000000..f1395d223c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/models/work_package_spec.rb
@@ -0,0 +1,60 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, type: :model do
+ describe '#backlogs_types' do
+ it 'should return all the ids of types that are configures to be considered backlogs types' do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [1], 'task_type' => 2 })
+
+ expect(WorkPackage.backlogs_types).to match_array([1, 2])
+ end
+
+ it 'should return an empty array if nothing is defined' do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({})
+
+ expect(WorkPackage.backlogs_types).to eq([])
+ end
+
+ it 'should reflect changes to the configuration' do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [1], 'task_type' => 2 })
+ expect(WorkPackage.backlogs_types).to match_array([1, 2])
+
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [3], 'task_type' => 4 })
+ expect(WorkPackage.backlogs_types).to match_array([3, 4])
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_burndown_charts_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_burndown_charts_routing_spec.rb
new file mode 100644
index 0000000000..bbe59b8823
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_burndown_charts_routing_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbBurndownChartsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/sprints/21/burndown_chart')).to route_to(controller: 'rb_burndown_charts',
+ action: 'show',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_impediments_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_impediments_routing_spec.rb
new file mode 100644
index 0000000000..658ad37fff
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_impediments_routing_spec.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbImpedimentsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(post('/projects/project_42/sprints/21/impediments')).to route_to(controller: 'rb_impediments',
+ action: 'create',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ it {
+ expect(put('/projects/project_42/sprints/21/impediments/85')).to route_to(controller: 'rb_impediments',
+ action: 'update',
+ project_id: 'project_42',
+ sprint_id: '21',
+ id: '85')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_master_backlogs_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_master_backlogs_routing_spec.rb
new file mode 100644
index 0000000000..109a3359f6
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_master_backlogs_routing_spec.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbMasterBacklogsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/backlogs')).to route_to(controller: 'rb_master_backlogs',
+ action: 'index',
+ project_id: 'project_42')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_queries_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_queries_routing_spec.rb
new file mode 100644
index 0000000000..b7ecc51d3c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_queries_routing_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbQueriesController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/sprints/21/query')).to route_to(controller: 'rb_queries',
+ action: 'show',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_sprints_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_sprints_routing_spec.rb
new file mode 100644
index 0000000000..dc05ef2bdc
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_sprints_routing_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbSprintsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(put('/projects/project_42/sprints/21')).to route_to(controller: 'rb_sprints',
+ action: 'update',
+ project_id: 'project_42',
+ id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_stories_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_stories_routing_spec.rb
new file mode 100644
index 0000000000..2942c1008c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_stories_routing_spec.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbStoriesController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(post('/projects/project_42/sprints/21/stories')).to route_to(controller: 'rb_stories',
+ action: 'create',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ it {
+ expect(put('/projects/project_42/sprints/21/stories/85')).to route_to(controller: 'rb_stories',
+ action: 'update',
+ project_id: 'project_42',
+ sprint_id: '21',
+ id: '85')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboard_card_configurations_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboard_card_configurations_routing_spec.rb
new file mode 100644
index 0000000000..f6f63ddc8e
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboard_card_configurations_routing_spec.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbExportCardConfigurationsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/sprints/21/export_card_configurations/10')).to route_to(controller: 'rb_export_card_configurations',
+ action: 'show',
+ project_id: 'project_42',
+ sprint_id: '21',
+ id: '10')
+ }
+
+ it {
+ expect(get('/projects/project_42/sprints/21/export_card_configurations')).to route_to(controller: 'rb_export_card_configurations',
+ action: 'index',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboards_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboards_routing_spec.rb
new file mode 100644
index 0000000000..bcee993417
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_taskboards_routing_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbTaskboardsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/sprints/21/taskboard')).to route_to(controller: 'rb_taskboards',
+ action: 'show',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_tasks_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_tasks_routing_spec.rb
new file mode 100644
index 0000000000..d7a71acc41
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_tasks_routing_spec.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbTasksController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(post('/projects/project_42/sprints/21/tasks')).to route_to(controller: 'rb_tasks',
+ action: 'create',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ it {
+ expect(put('/projects/project_42/sprints/21/tasks/85')).to route_to(controller: 'rb_tasks',
+ action: 'update',
+ project_id: 'project_42',
+ sprint_id: '21',
+ id: '85')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/routing/rb_wikis_routing_spec.rb b/vendored-plugins/openproject-backlogs/spec/routing/rb_wikis_routing_spec.rb
new file mode 100644
index 0000000000..a40769fa80
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/routing/rb_wikis_routing_spec.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe RbWikisController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/project_42/sprints/21/wiki')).to route_to(controller: 'rb_wikis',
+ action: 'show',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ it {
+ expect(get('/projects/project_42/sprints/21/wiki/edit')).to route_to(controller: 'rb_wikis',
+ action: 'edit',
+ project_id: 'project_42',
+ sprint_id: '21')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/spec_helper.rb b/vendored-plugins/openproject-backlogs/spec/spec_helper.rb
new file mode 100644
index 0000000000..96aeff84ba
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/spec_helper.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# -- load spec_helper from OpenProject core
+require 'spec_helper'
+
+# load gem dependencies
+require 'prawn'
+require 'pdf/inspector'
diff --git a/vendored-plugins/openproject-backlogs/spec/views/rb_burndown_charts/show_spec.rb b/vendored-plugins/openproject-backlogs/spec/views/rb_burndown_charts/show_spec.rb
new file mode 100644
index 0000000000..62d5276375
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/views/rb_burndown_charts/show_spec.rb
@@ -0,0 +1,128 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe 'rb_burndown_charts/show', type: :view do
+ let(:user1) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:role_allowed) {
+ FactoryGirl.create(:role,
+ permissions: [:create_impediments, :create_tasks, :update_impediments, :update_tasks])
+ }
+ let(:role_forbidden) { FactoryGirl.create(:role) }
+ # We need to create these as some view helpers access the database
+ let(:statuses) {
+ [FactoryGirl.create(:status),
+ FactoryGirl.create(:status),
+ FactoryGirl.create(:status)]
+ }
+
+ let(:type_task) { FactoryGirl.create(:type_task) }
+ let(:type_feature) { FactoryGirl.create(:type_feature) }
+ let(:issue_priority) { FactoryGirl.create(:priority) }
+ let(:project) do
+ project = FactoryGirl.create(:project, types: [type_feature, type_task])
+ project.members = [FactoryGirl.create(:member, principal: user1, project: project, roles: [role_allowed]),
+ FactoryGirl.create(:member, principal: user2, project: project, roles: [role_forbidden])]
+ project
+ end
+
+ let(:story_a) {
+ FactoryGirl.create(:story, status: statuses[0],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_b) {
+ FactoryGirl.create(:story, status: statuses[1],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_c) {
+ FactoryGirl.create(:story, status: statuses[2],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:stories) { [story_a, story_b, story_c] }
+ let(:sprint) { FactoryGirl.create(:sprint, project: project, start_date: Date.today - 1.week, effective_date: Date.today + 1.week) }
+ let(:task) do
+ task = FactoryGirl.create(:task, project: project, status: statuses[0], fixed_version: sprint, type: type_task)
+ # This is necessary as for some unknown reason passing the parent directly
+ # leads to the task searching for the parent with 'root_id' is NULL, which
+ # is not the case as the story has its own id as root_id
+ task.parent_id = story_a.id
+ task
+ end
+
+ before :each do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [type_feature.id], 'task_type' => type_task.id })
+ view.extend BurndownChartsHelper
+
+ # We directly force the creation of stories,statuses by calling the method
+ stories
+ end
+
+ describe 'burndown chart' do
+ it 'renders a version with dates' do
+ assign(:sprint, sprint)
+ assign(:burndown, sprint.burndown(project))
+ render
+
+ expect(view).to render_template(partial: '_burndown', count: 1)
+ end
+
+ it 'renders a version without dates' do
+ sprint.start_date = nil
+ sprint.effective_date = nil
+ sprint.save
+ assign(:sprint, sprint)
+ assign(:burndown, sprint.burndown(project))
+
+ render
+
+ expect(view).to render_template(partial: '_burndown', count: 0)
+ expect(rendered).to include(I18n.translate 'backlogs.no_burndown_data')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/views/rb_master_backlogs/index.html.erb_spec.rb b/vendored-plugins/openproject-backlogs/spec/views/rb_master_backlogs/index.html.erb_spec.rb
new file mode 100644
index 0000000000..7647ea6c06
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/views/rb_master_backlogs/index.html.erb_spec.rb
@@ -0,0 +1,127 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe 'rb_master_backlogs/index', type: :view do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:role_allowed) {
+ FactoryGirl.create(:role,
+ permissions: [:view_master_backlog, :view_taskboards])
+ }
+ let(:statuses) {
+ [FactoryGirl.create(:status, is_default: true),
+ FactoryGirl.create(:status),
+ FactoryGirl.create(:status)]
+ }
+ let(:type_task) { FactoryGirl.create(:type_task) }
+ let(:type_feature) { FactoryGirl.create(:type_feature) }
+ let(:issue_priority) { FactoryGirl.create(:priority) }
+ let(:project) do
+ project = FactoryGirl.create(:project, types: [type_feature, type_task])
+ project.members = [FactoryGirl.create(:member, principal: user, project: project, roles: [role_allowed])]
+ project
+ end
+ let(:story_a) {
+ FactoryGirl.create(:story, status: statuses[0],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_b) {
+ FactoryGirl.create(:story, status: statuses[1],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_c) {
+ FactoryGirl.create(:story, status: statuses[2],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:stories) { [story_a, story_b, story_c] }
+ let(:sprint) { FactoryGirl.create(:sprint, project: project) }
+
+ before :each do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [type_feature.id], 'task_type' => type_task.id })
+ view.extend RbCommonHelper
+ view.extend RbMasterBacklogsHelper
+ allow(view).to receive(:current_user).and_return(user)
+
+ assign(:project, project)
+ assign(:sprint, sprint)
+ assign(:owner_backlogs, Backlog.owner_backlogs(project))
+ assign(:sprint_backlogs, Backlog.sprint_backlogs(project))
+
+ allow(User).to receive(:current).and_return(user)
+
+ # We directly force the creation of stories by calling the method
+ stories
+ end
+
+ it 'shows link to export with the default export card configuration' do
+ default_export_card_config = FactoryGirl.create(:export_card_configuration)
+ assign(:export_card_config_meta, {
+ default: default_export_card_config,
+ count: 1 })
+
+ render
+
+ assert_select '.menu ul.items a' do |a|
+ url = backlogs_project_sprint_export_card_configuration_path(project.identifier, sprint.id, default_export_card_config.id, format: :pdf)
+ expect(a.last).to have_content 'Export'
+ expect(a.last).to have_css("a[href='#{url}']")
+ end
+ end
+
+ it 'shows link to display export card configuration choice modal' do
+ assign(:export_card_config_meta, { count: 2 })
+ render
+
+ assert_select '.menu ul.items a' do |a|
+ url = backlogs_project_sprint_export_card_configurations_path(project.id, sprint.id)
+ expect(a.last).to have_content 'Export'
+ expect(a.last).to have_css("a[href='#{url}']")
+ expect(a.last).to have_css('a[data-modal]')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/spec/views/rb_taskboards/show_spec.rb b/vendored-plugins/openproject-backlogs/spec/views/rb_taskboards/show_spec.rb
new file mode 100644
index 0000000000..4a3c17cc9d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/spec/views/rb_taskboards/show_spec.rb
@@ -0,0 +1,264 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe 'rb_taskboards/show', type: :view do
+ let(:user1) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:role_allowed) {
+ FactoryGirl.create(:role,
+ permissions: [:create_impediments, :create_tasks, :update_impediments, :update_tasks])
+ }
+ let(:role_forbidden) { FactoryGirl.create(:role) }
+ # We need to create these as some view helpers access the database
+ let(:statuses) {
+ [FactoryGirl.create(:status),
+ FactoryGirl.create(:status),
+ FactoryGirl.create(:status)]
+ }
+
+ let(:type_task) { FactoryGirl.create(:type_task) }
+ let(:type_feature) { FactoryGirl.create(:type_feature) }
+ let(:issue_priority) { FactoryGirl.create(:priority) }
+ let(:project) do
+ project = FactoryGirl.create(:project, types: [type_feature, type_task])
+ project.members = [FactoryGirl.create(:member, principal: user1, project: project, roles: [role_allowed]),
+ FactoryGirl.create(:member, principal: user2, project: project, roles: [role_forbidden])]
+ project
+ end
+
+ let(:story_a) {
+ FactoryGirl.create(:story, status: statuses[0],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_b) {
+ FactoryGirl.create(:story, status: statuses[1],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:story_c) {
+ FactoryGirl.create(:story, status: statuses[2],
+ project: project,
+ type: type_feature,
+ fixed_version: sprint,
+ priority: issue_priority
+ )
+ }
+ let(:stories) { [story_a, story_b, story_c] }
+ let(:sprint) { FactoryGirl.create(:sprint, project: project) }
+ let(:task) do
+ task = FactoryGirl.create(:task, project: project, status: statuses[0], fixed_version: sprint, type: type_task)
+ # This is necessary as for some unknown reason passing the parent directly
+ # leads to the task searching for the parent with 'root_id' is NULL, which
+ # is not the case as the story has its own id as root_id
+ task.parent_id = story_a.id
+ task
+ end
+ let(:impediment) { FactoryGirl.create(:impediment, project: project, status: statuses[0], fixed_version: sprint, blocks_ids: task.id.to_s, type: type_task) }
+
+ before :each do
+ allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ 'story_types' => [type_feature.id], 'task_type' => type_task.id })
+ view.extend RbCommonHelper
+ view.extend TaskboardsHelper
+
+ assign(:project, project)
+ assign(:sprint, sprint)
+ assign(:statuses, statuses)
+
+ # We directly force the creation of stories by calling the method
+ stories
+ end
+
+ describe 'story blocks' do
+ it 'contains the story id' do
+ render
+
+ stories.each do |story|
+ expect(rendered).to have_selector "#story_#{story.id}" do
+ with_selector '.id', Regexp.new(story.id.to_s)
+ end
+ end
+ end
+
+ it 'has a title containing the story subject' do
+ render
+
+ stories.each do |story|
+ expect(rendered).to have_selector "#story_#{story.id}" do
+ with_selector '.subject', story.subject
+ end
+ end
+ end
+
+ it 'contains the story status' do
+ render
+
+ stories.each do |story|
+ expect(rendered).to have_selector "#story_#{story.id}" do
+ with_selector '.status', story.status.name
+ end
+ end
+ end
+
+ it 'contains the user it is assigned to' do
+ render
+
+ stories.each do |story|
+ expect(rendered).to have_selector "#story_#{story.id}" do
+ with_selector '.assigned_to_id', assignee.name
+ end
+ end
+ end
+ end
+
+ describe 'create buttons' do
+ it 'renders clickable + buttons for all stories with the right permissions' do
+ allow(User).to receive(:current).and_return(user1)
+
+ render
+
+ stories.each do |story|
+ assert_select "tr.story_#{story.id} td.add_new" do |td|
+ expect(td.count).to eq 1
+ expect(td.first).to have_content '+'
+ expect(td.first).to have_css '.clickable'
+ end
+ end
+ end
+
+ it 'does not render a clickable + buttons for all stories without the right permissions' do
+ allow(User).to receive(:current).and_return(user2)
+
+ render
+
+ stories.each do |story|
+ assert_select "tr.story_#{story.id} td.add_new" do |td|
+ expect(td.count).to eq 1
+ expect(td.first).not_to have_content '+'
+ expect(td.first).not_to have_css '.clickable'
+ end
+ end
+ end
+
+ it 'renders clickable + buttons for impediments with the right permissions' do
+ allow(User).to receive(:current).and_return(user1)
+
+ render
+
+ stories.each do |_story|
+ assert_select '#impediments td.add_new' do |td|
+ expect(td.count).to eq 1
+ expect(td.first).to have_content '+'
+ expect(td.first).to have_css '.clickable'
+ end
+ end
+ end
+
+ it 'does not render a clickable + buttons for impediments without the right permissions' do
+ allow(User).to receive(:current).and_return(user2)
+
+ render
+
+ stories.each do |_story|
+ assert_select '#impediments td.add_new' do |td|
+ expect(td.count).to eq 1
+ expect(td.first).not_to have_content '+'
+ expect(td.first).not_to have_css '.clickable'
+ end
+ end
+ end
+ end
+
+ describe 'update tasks or impediments' do
+ it 'allows edit and drag for all tasks with the right permissions' do
+ allow(User).to receive(:current).and_return(user1)
+ task
+ impediment
+ render
+
+ assert_select '.model.work_package.task' do |task|
+ expect(task.count).to eq 1
+ expect(task.first).not_to have_css '.task.prevent_edit'
+ end
+ end
+
+ it 'does not allow to edit and drag for all tasks without the right permissions' do
+ allow(User).to receive(:current).and_return(user2)
+ task
+ impediment
+
+ render
+
+ assert_select '.model.work_package.task' do |task|
+ expect(task.count).to eq 1
+ expect(task.first).to have_css '.task.prevent_edit'
+ end
+ end
+
+ it 'allows edit and drag for all impediments with the right permissions' do
+ allow(User).to receive(:current).and_return(user1)
+ task
+ impediment
+
+ render
+
+ assert_select '.model.work_package.impediment' do |impediment|
+ expect(impediment.count).to eq 3 # 2 additional for the task and the invisible form
+ expect(impediment.first).not_to have_css '.impediment.prevent_edit'
+ end
+ end
+
+ it 'does not allow to edit and drag for all impediments without the right permissions' do
+ allow(User).to receive(:current).and_return(user2)
+ task
+ impediment
+
+ render
+
+ assert_select '.model.work_package.impediment' do |impediment|
+ expect(impediment.count).to eq 3 # 2 additional for the task and the invisible form
+ expect(impediment.first).to have_css '.impediment.prevent_edit'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.cookie.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.cookie.js
new file mode 100644
index 0000000000..6df1faca25
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.cookie.js
@@ -0,0 +1,96 @@
+/**
+ * Cookie plugin
+ *
+ * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ */
+
+/**
+ * Create a cookie with the given name and value and other optional parameters.
+ *
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Set the value of a cookie.
+ * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
+ * @desc Create a cookie with all available options.
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Create a session cookie.
+ * @example $.cookie('the_cookie', null);
+ * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
+ * used when the cookie was set.
+ *
+ * @param String name The name of the cookie.
+ * @param String value The value of the cookie.
+ * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
+ * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
+ * If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
+ * If set to null or omitted, the cookie will be a session cookie and will not be retained
+ * when the the browser exits.
+ * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
+ * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
+ * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
+ * require a secure protocol (like HTTPS).
+ * @type undefined
+ *
+ * @name $.cookie
+ * @cat Plugins/Cookie
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
+ */
+
+/**
+ * Get the value of a cookie with the given name.
+ *
+ * @example $.cookie('the_cookie');
+ * @desc Get the value of a cookie.
+ *
+ * @param String name The name of the cookie.
+ * @return The value of the cookie.
+ * @type String
+ *
+ * @name $.cookie
+ * @cat Plugins/Cookie
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
+ */
+jQuery.cookie = function(name, value, options) {
+ if (typeof value != 'undefined') { // name and value given, set cookie
+ options = options || {};
+ if (value === null) {
+ value = '';
+ options.expires = -1;
+ }
+ var expires = '';
+ if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
+ var date;
+ if (typeof options.expires == 'number') {
+ date = new Date();
+ date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
+ } else {
+ date = options.expires;
+ }
+ expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
+ }
+ // CAUTION: Needed to parenthesize options.path and options.domain
+ // in the following expressions, otherwise they evaluate to undefined
+ // in the packed version for some reason...
+ var path = options.path ? '; path=' + (options.path) : '';
+ var domain = options.domain ? '; domain=' + (options.domain) : '';
+ var secure = options.secure ? '; secure' : '';
+ document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
+ } else { // only name given, get cookie
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+};
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/API.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/API.txt
new file mode 100644
index 0000000000..8a8dbc23d2
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/API.txt
@@ -0,0 +1,1201 @@
+Flot Reference
+--------------
+
+Consider a call to the plot function:
+
+ var plot = $.plot(placeholder, data, options)
+
+The placeholder is a jQuery object or DOM element or jQuery expression
+that the plot will be put into. This placeholder needs to have its
+width and height set as explained in the README (go read that now if
+you haven't, it's short). The plot will modify some properties of the
+placeholder so it's recommended you simply pass in a div that you
+don't use for anything else. Make sure you check any fancy styling
+you apply to the div, e.g. background images have been reported to be a
+problem on IE 7.
+
+The format of the data is documented below, as is the available
+options. The plot object returned from the call has some methods you
+can call. These are documented separately below.
+
+Note that in general Flot gives no guarantees if you change any of the
+objects you pass in to the plot function or get out of it since
+they're not necessarily deep-copied.
+
+
+Data Format
+-----------
+
+The data is an array of data series:
+
+ [ series1, series2, ... ]
+
+A series can either be raw data or an object with properties. The raw
+data format is an array of points:
+
+ [ [x1, y1], [x2, y2], ... ]
+
+E.g.
+
+ [ [1, 3], [2, 14.01], [3.5, 3.14] ]
+
+Note that to simplify the internal logic in Flot both the x and y
+values must be numbers (even if specifying time series, see below for
+how to do this). This is a common problem because you might retrieve
+data from the database and serialize them directly to JSON without
+noticing the wrong type. If you're getting mysterious errors, double
+check that you're inputting numbers and not strings.
+
+If a null is specified as a point or if one of the coordinates is null
+or couldn't be converted to a number, the point is ignored when
+drawing. As a special case, a null value for lines is interpreted as a
+line segment end, i.e. the points before and after the null value are
+not connected.
+
+Lines and points take two coordinates. For filled lines and bars, you
+can specify a third coordinate which is the bottom of the filled
+area/bar (defaults to 0).
+
+The format of a single series object is as follows:
+
+ {
+ color: color or number
+ data: rawdata
+ label: string
+ lines: specific lines options
+ bars: specific bars options
+ points: specific points options
+ xaxis: number
+ yaxis: number
+ clickable: boolean
+ hoverable: boolean
+ shadowSize: number
+ }
+
+You don't have to specify any of them except the data, the rest are
+options that will get default values. Typically you'd only specify
+label and data, like this:
+
+ {
+ label: "y = 3",
+ data: [[0, 3], [10, 3]]
+ }
+
+The label is used for the legend, if you don't specify one, the series
+will not show up in the legend.
+
+If you don't specify color, the series will get a color from the
+auto-generated colors. The color is either a CSS color specification
+(like "rgb(255, 100, 123)") or an integer that specifies which of
+auto-generated colors to select, e.g. 0 will get color no. 0, etc.
+
+The latter is mostly useful if you let the user add and remove series,
+in which case you can hard-code the color index to prevent the colors
+from jumping around between the series.
+
+The "xaxis" and "yaxis" options specify which axis to use. The axes
+are numbered from 1 (default), so { yaxis: 2} means that the series
+should be plotted against the second y axis.
+
+"clickable" and "hoverable" can be set to false to disable
+interactivity for specific series if interactivity is turned on in
+the plot, see below.
+
+The rest of the options are all documented below as they are the same
+as the default options passed in via the options parameter in the plot
+commmand. When you specify them for a specific data series, they will
+override the default options for the plot for that data series.
+
+Here's a complete example of a simple data specification:
+
+ [ { label: "Foo", data: [ [10, 1], [17, -14], [30, 5] ] },
+ { label: "Bar", data: [ [11, 13], [19, 11], [30, -7] ] } ]
+
+
+Plot Options
+------------
+
+All options are completely optional. They are documented individually
+below, to change them you just specify them in an object, e.g.
+
+ var options = {
+ series: {
+ lines: { show: true },
+ points: { show: true }
+ }
+ };
+
+ $.plot(placeholder, data, options);
+
+
+Customizing the legend
+======================
+
+ legend: {
+ show: boolean
+ labelFormatter: null or (fn: string, series object -> string)
+ labelBoxBorderColor: color
+ noColumns: number
+ position: "ne" or "nw" or "se" or "sw"
+ margin: number of pixels or [x margin, y margin]
+ backgroundColor: null or color
+ backgroundOpacity: number between 0 and 1
+ container: null or jQuery object/DOM element/jQuery expression
+ }
+
+The legend is generated as a table with the data series labels and
+small label boxes with the color of the series. If you want to format
+the labels in some way, e.g. make them to links, you can pass in a
+function for "labelFormatter". Here's an example that makes them
+clickable:
+
+ labelFormatter: function(label, series) {
+ // series is the series object for the label
+ return '' + label + ' ';
+ }
+
+"noColumns" is the number of columns to divide the legend table into.
+"position" specifies the overall placement of the legend within the
+plot (top-right, top-left, etc.) and margin the distance to the plot
+edge (this can be either a number or an array of two numbers like [x,
+y]). "backgroundColor" and "backgroundOpacity" specifies the
+background. The default is a partly transparent auto-detected
+background.
+
+If you want the legend to appear somewhere else in the DOM, you can
+specify "container" as a jQuery object/expression to put the legend
+table into. The "position" and "margin" etc. options will then be
+ignored. Note that Flot will overwrite the contents of the container.
+
+
+Customizing the axes
+====================
+
+ xaxis, yaxis: {
+ show: null or true/false
+ position: "bottom" or "top" or "left" or "right"
+ mode: null or "time"
+
+ color: null or color spec
+ tickColor: null or color spec
+
+ min: null or number
+ max: null or number
+ autoscaleMargin: null or number
+
+ transform: null or fn: number -> number
+ inverseTransform: null or fn: number -> number
+
+ ticks: null or number or ticks array or (fn: range -> ticks array)
+ tickSize: number or array
+ minTickSize: number or array
+ tickFormatter: (fn: number, object -> string) or string
+ tickDecimals: null or number
+
+ labelWidth: null or number
+ labelHeight: null or number
+ reserveSpace: null or true
+
+ tickLength: null or number
+
+ alignTicksWithAxis: null or number
+ }
+
+All axes have the same kind of options. The following describes how to
+configure one axis, see below for what to do if you've got more than
+one x axis or y axis.
+
+If you don't set the "show" option (i.e. it is null), visibility is
+auto-detected, i.e. the axis will show up if there's data associated
+with it. You can override this by setting the "show" option to true or
+false.
+
+The "position" option specifies where the axis is placed, bottom or
+top for x axes, left or right for y axes. The "mode" option determines
+how the data is interpreted, the default of null means as decimal
+numbers. Use "time" for time series data, see the time series data
+section.
+
+The "color" option determines the color of the labels and ticks for
+the axis (default is the grid color). For more fine-grained control
+you can also set the color of the ticks separately with "tickColor"
+(otherwise it's autogenerated as the base color with some
+transparency).
+
+The options "min"/"max" are the precise minimum/maximum value on the
+scale. If you don't specify either of them, a value will automatically
+be chosen based on the minimum/maximum data values. Note that Flot
+always examines all the data values you feed to it, even if a
+restriction on another axis may make some of them invisible (this
+makes interactive use more stable).
+
+The "autoscaleMargin" is a bit esoteric: it's the fraction of margin
+that the scaling algorithm will add to avoid that the outermost points
+ends up on the grid border. Note that this margin is only applied when
+a min or max value is not explicitly set. If a margin is specified,
+the plot will furthermore extend the axis end-point to the nearest
+whole tick. The default value is "null" for the x axes and 0.02 for y
+axes which seems appropriate for most cases.
+
+"transform" and "inverseTransform" are callbacks you can put in to
+change the way the data is drawn. You can design a function to
+compress or expand certain parts of the axis non-linearly, e.g.
+suppress weekends or compress far away points with a logarithm or some
+other means. When Flot draws the plot, each value is first put through
+the transform function. Here's an example, the x axis can be turned
+into a natural logarithm axis with the following code:
+
+ xaxis: {
+ transform: function (v) { return Math.log(v); },
+ inverseTransform: function (v) { return Math.exp(v); }
+ }
+
+Similarly, for reversing the y axis so the values appear in inverse
+order:
+
+ yaxis: {
+ transform: function (v) { return -v; },
+ inverseTransform: function (v) { return -v; }
+ }
+
+Note that for finding extrema, Flot assumes that the transform
+function does not reorder values (it should be monotone).
+
+The inverseTransform is simply the inverse of the transform function
+(so v == inverseTransform(transform(v)) for all relevant v). It is
+required for converting from canvas coordinates to data coordinates,
+e.g. for a mouse interaction where a certain pixel is clicked. If you
+don't use any interactive features of Flot, you may not need it.
+
+
+The rest of the options deal with the ticks.
+
+If you don't specify any ticks, a tick generator algorithm will make
+some for you. The algorithm has two passes. It first estimates how
+many ticks would be reasonable and uses this number to compute a nice
+round tick interval size. Then it generates the ticks.
+
+You can specify how many ticks the algorithm aims for by setting
+"ticks" to a number. The algorithm always tries to generate reasonably
+round tick values so even if you ask for three ticks, you might get
+five if that fits better with the rounding. If you don't want any
+ticks at all, set "ticks" to 0 or an empty array.
+
+Another option is to skip the rounding part and directly set the tick
+interval size with "tickSize". If you set it to 2, you'll get ticks at
+2, 4, 6, etc. Alternatively, you can specify that you just don't want
+ticks at a size less than a specific tick size with "minTickSize".
+Note that for time series, the format is an array like [2, "month"],
+see the next section.
+
+If you want to completely override the tick algorithm, you can specify
+an array for "ticks", either like this:
+
+ ticks: [0, 1.2, 2.4]
+
+Or like this where the labels are also customized:
+
+ ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]]
+
+You can mix the two if you like.
+
+For extra flexibility you can specify a function as the "ticks"
+parameter. The function will be called with an object with the axis
+min and max and should return a ticks array. Here's a simplistic tick
+generator that spits out intervals of pi, suitable for use on the x
+axis for trigonometric functions:
+
+ function piTickGenerator(axis) {
+ var res = [], i = Math.floor(axis.min / Math.PI);
+ do {
+ var v = i * Math.PI;
+ res.push([v, i + "\u03c0"]);
+ ++i;
+ } while (v < axis.max);
+
+ return res;
+ }
+
+You can control how the ticks look like with "tickDecimals", the
+number of decimals to display (default is auto-detected).
+
+Alternatively, for ultimate control over how ticks are formatted you can
+provide a function to "tickFormatter". The function is passed two
+parameters, the tick value and an axis object with information, and
+should return a string. The default formatter looks like this:
+
+ function formatter(val, axis) {
+ return val.toFixed(axis.tickDecimals);
+ }
+
+The axis object has "min" and "max" with the range of the axis,
+"tickDecimals" with the number of decimals to round the value to and
+"tickSize" with the size of the interval between ticks as calculated
+by the automatic axis scaling algorithm (or specified by you). Here's
+an example of a custom formatter:
+
+ function suffixFormatter(val, axis) {
+ if (val > 1000000)
+ return (val / 1000000).toFixed(axis.tickDecimals) + " MB";
+ else if (val > 1000)
+ return (val / 1000).toFixed(axis.tickDecimals) + " kB";
+ else
+ return val.toFixed(axis.tickDecimals) + " B";
+ }
+
+"labelWidth" and "labelHeight" specifies a fixed size of the tick
+labels in pixels. They're useful in case you need to align several
+plots. "reserveSpace" means that even if an axis isn't shown, Flot
+should reserve space for it - it is useful in combination with
+labelWidth and labelHeight for aligning multi-axis charts.
+
+"tickLength" is the length of the tick lines in pixels. By default, the
+innermost axes will have ticks that extend all across the plot, while
+any extra axes use small ticks. A value of null means use the default,
+while a number means small ticks of that length - set it to 0 to hide
+the lines completely.
+
+If you set "alignTicksWithAxis" to the number of another axis, e.g.
+alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks
+of this axis are aligned with the ticks of the other axis. This may
+improve the looks, e.g. if you have one y axis to the left and one to
+the right, because the grid lines will then match the ticks in both
+ends. The trade-off is that the forced ticks won't necessarily be at
+natural places.
+
+
+Multiple axes
+=============
+
+If you need more than one x axis or y axis, you need to specify for
+each data series which axis they are to use, as described under the
+format of the data series, e.g. { data: [...], yaxis: 2 } specifies
+that a series should be plotted against the second y axis.
+
+To actually configure that axis, you can't use the xaxis/yaxis options
+directly - instead there are two arrays in the options:
+
+ xaxes: []
+ yaxes: []
+
+Here's an example of configuring a single x axis and two y axes (we
+can leave options of the first y axis empty as the defaults are fine):
+
+ {
+ xaxes: [ { position: "top" } ],
+ yaxes: [ { }, { position: "right", min: 20 } ]
+ }
+
+The arrays get their default values from the xaxis/yaxis settings, so
+say you want to have all y axes start at zero, you can simply specify
+yaxis: { min: 0 } instead of adding a min parameter to all the axes.
+
+Generally, the various interfaces in Flot dealing with data points
+either accept an xaxis/yaxis parameter to specify which axis number to
+use (starting from 1), or lets you specify the coordinate directly as
+x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis".
+
+
+Time series data
+================
+
+Time series are a bit more difficult than scalar data because
+calendars don't follow a simple base 10 system. For many cases, Flot
+abstracts most of this away, but it can still be a bit difficult to
+get the data into Flot. So we'll first discuss the data format.
+
+The time series support in Flot is based on Javascript timestamps,
+i.e. everywhere a time value is expected or handed over, a Javascript
+timestamp number is used. This is a number, not a Date object. A
+Javascript timestamp is the number of milliseconds since January 1,
+1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's
+in milliseconds, so remember to multiply by 1000!
+
+You can see a timestamp like this
+
+ alert((new Date()).getTime())
+
+Normally you want the timestamps to be displayed according to a
+certain time zone, usually the time zone in which the data has been
+produced. However, Flot always displays timestamps according to UTC.
+It has to as the only alternative with core Javascript is to interpret
+the timestamps according to the time zone that the visitor is in,
+which means that the ticks will shift unpredictably with the time zone
+and daylight savings of each visitor.
+
+So given that there's no good support for custom time zones in
+Javascript, you'll have to take care of this server-side.
+
+The easiest way to think about it is to pretend that the data
+production time zone is UTC, even if it isn't. So if you have a
+datapoint at 2002-02-20 08:00, you can generate a timestamp for eight
+o'clock UTC even if it really happened eight o'clock UTC+0200.
+
+In PHP you can get an appropriate timestamp with
+'strtotime("2002-02-20 UTC") * 1000', in Python with
+'calendar.timegm(datetime_object.timetuple()) * 1000', in .NET with
+something like:
+
+ public static int GetJavascriptTimestamp(System.DateTime input)
+ {
+ System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks);
+ System.DateTime time = input.Subtract(span);
+ return (long)(time.Ticks / 10000);
+ }
+
+Javascript also has some support for parsing date strings, so it is
+possible to generate the timestamps manually client-side.
+
+If you've already got the real UTC timestamp, it's too late to use the
+pretend trick described above. But you can fix up the timestamps by
+adding the time zone offset, e.g. for UTC+0200 you would add 2 hours
+to the UTC timestamp you got. Then it'll look right on the plot. Most
+programming environments have some means of getting the timezone
+offset for a specific date (note that you need to get the offset for
+each individual timestamp to account for daylight savings).
+
+Once you've gotten the timestamps into the data and specified "time"
+as the axis mode, Flot will automatically generate relevant ticks and
+format them. As always, you can tweak the ticks via the "ticks" option
+- just remember that the values should be timestamps (numbers), not
+Date objects.
+
+Tick generation and formatting can also be controlled separately
+through the following axis options:
+
+ minTickSize: array
+ timeformat: null or format string
+ monthNames: null or array of size 12 of strings
+ twelveHourClock: boolean
+
+Here "timeformat" is a format string to use. You might use it like
+this:
+
+ xaxis: {
+ mode: "time"
+ timeformat: "%y/%m/%d"
+ }
+
+This will result in tick labels like "2000/12/24". The following
+specifiers are supported
+
+ %h: hours
+ %H: hours (left-padded with a zero)
+ %M: minutes (left-padded with a zero)
+ %S: seconds (left-padded with a zero)
+ %d: day of month (1-31), use %0d for zero-padding
+ %m: month (1-12), use %0m for zero-padding
+ %y: year (four digits)
+ %b: month name (customizable)
+ %p: am/pm, additionally switches %h/%H to 12 hour instead of 24
+ %P: AM/PM (uppercase version of %p)
+
+Inserting a zero like %0m or %0d means that the specifier will be
+left-padded with a zero if it's only single-digit. So %y-%0m-%0d
+results in unambigious ISO timestamps like 2007-05-10 (for May 10th).
+
+You can customize the month names with the "monthNames" option. For
+instance, for Danish you might specify:
+
+ monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"]
+
+If you set "twelveHourClock" to true, the autogenerated timestamps
+will use 12 hour AM/PM timestamps instead of 24 hour.
+
+The format string and month names are used by a very simple built-in
+format function that takes a date object, a format string (and
+optionally an array of month names) and returns the formatted string.
+If needed, you can access it as $.plot.formatDate(date, formatstring,
+monthNames) or even replace it with another more advanced function
+from a date library if you're feeling adventurous.
+
+If everything else fails, you can control the formatting by specifying
+a custom tick formatter function as usual. Here's a simple example
+which will format December 24 as 24/12:
+
+ tickFormatter: function (val, axis) {
+ var d = new Date(val);
+ return d.getUTCDate() + "/" + (d.getUTCMonth() + 1);
+ }
+
+Note that for the time mode "tickSize" and "minTickSize" are a bit
+special in that they are arrays on the form "[value, unit]" where unit
+is one of "second", "minute", "hour", "day", "month" and "year". So
+you can specify
+
+ minTickSize: [1, "month"]
+
+to get a tick interval size of at least 1 month and correspondingly,
+if axis.tickSize is [2, "day"] in the tick formatter, the ticks have
+been produced with two days in-between.
+
+
+
+Customizing the data series
+===========================
+
+ series: {
+ lines, points, bars: {
+ show: boolean
+ lineWidth: number
+ fill: boolean or number
+ fillColor: null or color/gradient
+ }
+
+ points: {
+ radius: number
+ symbol: "circle" or function
+ }
+
+ bars: {
+ barWidth: number
+ align: "left" or "center"
+ horizontal: boolean
+ }
+
+ lines: {
+ steps: boolean
+ }
+
+ shadowSize: number
+ }
+
+ colors: [ color1, color2, ... ]
+
+The options inside "series: {}" are copied to each of the series. So
+you can specify that all series should have bars by putting it in the
+global options, or override it for individual series by specifying
+bars in a particular the series object in the array of data.
+
+The most important options are "lines", "points" and "bars" that
+specify whether and how lines, points and bars should be shown for
+each data series. In case you don't specify anything at all, Flot will
+default to showing lines (you can turn this off with
+lines: { show: false }). You can specify the various types
+independently of each other, and Flot will happily draw each of them
+in turn (this is probably only useful for lines and points), e.g.
+
+ var options = {
+ series: {
+ lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" },
+ points: { show: true, fill: false }
+ }
+ };
+
+"lineWidth" is the thickness of the line or outline in pixels. You can
+set it to 0 to prevent a line or outline from being drawn; this will
+also hide the shadow.
+
+"fill" is whether the shape should be filled. For lines, this produces
+area graphs. You can use "fillColor" to specify the color of the fill.
+If "fillColor" evaluates to false (default for everything except
+points which are filled with white), the fill color is auto-set to the
+color of the data series. You can adjust the opacity of the fill by
+setting fill to a number between 0 (fully transparent) and 1 (fully
+opaque).
+
+For bars, fillColor can be a gradient, see the gradient documentation
+below. "barWidth" is the width of the bars in units of the x axis (or
+the y axis if "horizontal" is true), contrary to most other measures
+that are specified in pixels. For instance, for time series the unit
+is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of
+a day. "align" specifies whether a bar should be left-aligned
+(default) or centered on top of the value it represents. When
+"horizontal" is on, the bars are drawn horizontally, i.e. from the y
+axis instead of the x axis; note that the bar end points are still
+defined in the same way so you'll probably want to swap the
+coordinates if you've been plotting vertical bars first.
+
+For lines, "steps" specifies whether two adjacent data points are
+connected with a straight (possibly diagonal) line or with first a
+horizontal and then a vertical line. Note that this transforms the
+data by adding extra points.
+
+For points, you can specify the radius and the symbol. The only
+built-in symbol type is circles, for other types you can use a plugin
+or define them yourself by specifying a callback:
+
+ function cross(ctx, x, y, radius, shadow) {
+ var size = radius * Math.sqrt(Math.PI) / 2;
+ ctx.moveTo(x - size, y - size);
+ ctx.lineTo(x + size, y + size);
+ ctx.moveTo(x - size, y + size);
+ ctx.lineTo(x + size, y - size);
+ }
+
+The parameters are the drawing context, x and y coordinates of the
+center of the point, a radius which corresponds to what the circle
+would have used and whether the call is to draw a shadow (due to
+limited canvas support, shadows are currently faked through extra
+draws). It's good practice to ensure that the area covered by the
+symbol is the same as for the circle with the given radius, this
+ensures that all symbols have approximately the same visual weight.
+
+"shadowSize" is the default size of shadows in pixels. Set it to 0 to
+remove shadows.
+
+The "colors" array specifies a default color theme to get colors for
+the data series from. You can specify as many colors as you like, like
+this:
+
+ colors: ["#d18b2c", "#dba255", "#919733"]
+
+If there are more data series than colors, Flot will try to generate
+extra colors by lightening and darkening colors in the theme.
+
+
+Customizing the grid
+====================
+
+ grid: {
+ show: boolean
+ aboveData: boolean
+ color: color
+ backgroundColor: color/gradient or null
+ labelMargin: number
+ axisMargin: number
+ markings: array of markings or (fn: axes -> array of markings)
+ borderWidth: number
+ borderColor: color or null
+ minBorderMargin: number or null
+ clickable: boolean
+ hoverable: boolean
+ autoHighlight: boolean
+ mouseActiveRadius: number
+ }
+
+The grid is the thing with the axes and a number of ticks. Many of the
+things in the grid are configured under the individual axes, but not
+all. "color" is the color of the grid itself whereas "backgroundColor"
+specifies the background color inside the grid area, here null means
+that the background is transparent. You can also set a gradient, see
+the gradient documentation below.
+
+You can turn off the whole grid including tick labels by setting
+"show" to false. "aboveData" determines whether the grid is drawn
+above the data or below (below is default).
+
+"labelMargin" is the space in pixels between tick labels and axis
+line, and "axisMargin" is the space in pixels between axes when there
+are two next to each other. Note that you can style the tick labels
+with CSS, e.g. to change the color. They have class "tickLabel".
+
+"borderWidth" is the width of the border around the plot. Set it to 0
+to disable the border. You can also set "borderColor" if you want the
+border to have a different color than the grid lines.
+"minBorderMargin" controls the default minimum margin around the
+border - it's used to make sure that points aren't accidentally
+clipped by the canvas edge so by default the value is computed from
+the point radius.
+
+"markings" is used to draw simple lines and rectangular areas in the
+background of the plot. You can either specify an array of ranges on
+the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple
+axes, you can specify coordinates for other axes instead, e.g. as
+x2axis/x3axis/...) or with a function that returns such an array given
+the axes for the plot in an object as the first parameter.
+
+You can set the color of markings by specifying "color" in the ranges
+object. Here's an example array:
+
+ markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ]
+
+If you leave out one of the values, that value is assumed to go to the
+border of the plot. So for example if you only specify { xaxis: {
+from: 0, to: 2 } } it means an area that extends from the top to the
+bottom of the plot in the x range 0-2.
+
+A line is drawn if from and to are the same, e.g.
+
+ markings: [ { yaxis: { from: 1, to: 1 } }, ... ]
+
+would draw a line parallel to the x axis at y = 1. You can control the
+line width with "lineWidth" in the range object.
+
+An example function that makes vertical stripes might look like this:
+
+ markings: function (axes) {
+ var markings = [];
+ for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2)
+ markings.push({ xaxis: { from: x, to: x + 1 } });
+ return markings;
+ }
+
+
+If you set "clickable" to true, the plot will listen for click events
+on the plot area and fire a "plotclick" event on the placeholder with
+a position and a nearby data item object as parameters. The coordinates
+are available both in the unit of the axes (not in pixels) and in
+global screen coordinates.
+
+Likewise, if you set "hoverable" to true, the plot will listen for
+mouse move events on the plot area and fire a "plothover" event with
+the same parameters as the "plotclick" event. If "autoHighlight" is
+true (the default), nearby data items are highlighted automatically.
+If needed, you can disable highlighting and control it yourself with
+the highlight/unhighlight plot methods described elsewhere.
+
+You can use "plotclick" and "plothover" events like this:
+
+ $.plot($("#placeholder"), [ d ], { grid: { clickable: true } });
+
+ $("#placeholder").bind("plotclick", function (event, pos, item) {
+ alert("You clicked at " + pos.x + ", " + pos.y);
+ // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ...
+ // if you need global screen coordinates, they are pos.pageX, pos.pageY
+
+ if (item) {
+ highlight(item.series, item.datapoint);
+ alert("You clicked a point!");
+ }
+ });
+
+The item object in this example is either null or a nearby object on the form:
+
+ item: {
+ datapoint: the point, e.g. [0, 2]
+ dataIndex: the index of the point in the data array
+ series: the series object
+ seriesIndex: the index of the series
+ pageX, pageY: the global screen coordinates of the point
+ }
+
+For instance, if you have specified the data like this
+
+ $.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...);
+
+and the mouse is near the point (7, 3), "datapoint" is [7, 3],
+"dataIndex" will be 1, "series" is a normalized series object with
+among other things the "Foo" label in series.label and the color in
+series.color, and "seriesIndex" is 0. Note that plugins and options
+that transform the data can shift the indexes from what you specified
+in the original data array.
+
+If you use the above events to update some other information and want
+to clear out that info in case the mouse goes away, you'll probably
+also need to listen to "mouseout" events on the placeholder div.
+
+"mouseActiveRadius" specifies how far the mouse can be from an item
+and still activate it. If there are two or more points within this
+radius, Flot chooses the closest item. For bars, the top-most bar
+(from the latest specified data series) is chosen.
+
+If you want to disable interactivity for a specific data series, you
+can set "hoverable" and "clickable" to false in the options for that
+series, like this { data: [...], label: "Foo", clickable: false }.
+
+
+Specifying gradients
+====================
+
+A gradient is specified like this:
+
+ { colors: [ color1, color2, ... ] }
+
+For instance, you might specify a background on the grid going from
+black to gray like this:
+
+ grid: {
+ backgroundColor: { colors: ["#000", "#999"] }
+ }
+
+For the series you can specify the gradient as an object that
+specifies the scaling of the brightness and the opacity of the series
+color, e.g.
+
+ { colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] }
+
+where the first color simply has its alpha scaled, whereas the second
+is also darkened. For instance, for bars the following makes the bars
+gradually disappear, without outline:
+
+ bars: {
+ show: true,
+ lineWidth: 0,
+ fill: true,
+ fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] }
+ }
+
+Flot currently only supports vertical gradients drawn from top to
+bottom because that's what works with IE.
+
+
+Plot Methods
+------------
+
+The Plot object returned from the plot function has some methods you
+can call:
+
+ - highlight(series, datapoint)
+
+ Highlight a specific datapoint in the data series. You can either
+ specify the actual objects, e.g. if you got them from a
+ "plotclick" event, or you can specify the indices, e.g.
+ highlight(1, 3) to highlight the fourth point in the second series
+ (remember, zero-based indexing).
+
+
+ - unhighlight(series, datapoint) or unhighlight()
+
+ Remove the highlighting of the point, same parameters as
+ highlight.
+
+ If you call unhighlight with no parameters, e.g. as
+ plot.unhighlight(), all current highlights are removed.
+
+
+ - setData(data)
+
+ You can use this to reset the data used. Note that axis scaling,
+ ticks, legend etc. will not be recomputed (use setupGrid() to do
+ that). You'll probably want to call draw() afterwards.
+
+ You can use this function to speed up redrawing a small plot if
+ you know that the axes won't change. Put in the new data with
+ setData(newdata), call draw(), and you're good to go. Note that
+ for large datasets, almost all the time is consumed in draw()
+ plotting the data so in this case don't bother.
+
+
+ - setupGrid()
+
+ Recalculate and set axis scaling, ticks, legend etc.
+
+ Note that because of the drawing model of the canvas, this
+ function will immediately redraw (actually reinsert in the DOM)
+ the labels and the legend, but not the actual tick lines because
+ they're drawn on the canvas. You need to call draw() to get the
+ canvas redrawn.
+
+ - draw()
+
+ Redraws the plot canvas.
+
+ - triggerRedrawOverlay()
+
+ Schedules an update of an overlay canvas used for drawing
+ interactive things like a selection and point highlights. This
+ is mostly useful for writing plugins. The redraw doesn't happen
+ immediately, instead a timer is set to catch multiple successive
+ redraws (e.g. from a mousemove). You can get to the overlay by
+ setting up a drawOverlay hook.
+
+ - width()/height()
+
+ Gets the width and height of the plotting area inside the grid.
+ This is smaller than the canvas or placeholder dimensions as some
+ extra space is needed (e.g. for labels).
+
+ - offset()
+
+ Returns the offset of the plotting area inside the grid relative
+ to the document, useful for instance for calculating mouse
+ positions (event.pageX/Y minus this offset is the pixel position
+ inside the plot).
+
+ - pointOffset({ x: xpos, y: ypos })
+
+ Returns the calculated offset of the data point at (x, y) in data
+ space within the placeholder div. If you are working with multiple axes, you
+ can specify the x and y axis references, e.g.
+
+ o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 })
+ // o.left and o.top now contains the offset within the div
+
+ - resize()
+
+ Tells Flot to resize the drawing canvas to the size of the
+ placeholder. You need to run setupGrid() and draw() afterwards as
+ canvas resizing is a destructive operation. This is used
+ internally by the resize plugin.
+
+ - shutdown()
+
+ Cleans up any event handlers Flot has currently registered. This
+ is used internally.
+
+
+There are also some members that let you peek inside the internal
+workings of Flot which is useful in some cases. Note that if you change
+something in the objects returned, you're changing the objects used by
+Flot to keep track of its state, so be careful.
+
+ - getData()
+
+ Returns an array of the data series currently used in normalized
+ form with missing settings filled in according to the global
+ options. So for instance to find out what color Flot has assigned
+ to the data series, you could do this:
+
+ var series = plot.getData();
+ for (var i = 0; i < series.length; ++i)
+ alert(series[i].color);
+
+ A notable other interesting field besides color is datapoints
+ which has a field "points" with the normalized data points in a
+ flat array (the field "pointsize" is the increment in the flat
+ array to get to the next point so for a dataset consisting only of
+ (x,y) pairs it would be 2).
+
+ - getAxes()
+
+ Gets an object with the axes. The axes are returned as the
+ attributes of the object, so for instance getAxes().xaxis is the
+ x axis.
+
+ Various things are stuffed inside an axis object, e.g. you could
+ use getAxes().xaxis.ticks to find out what the ticks are for the
+ xaxis. Two other useful attributes are p2c and c2p, functions for
+ transforming from data point space to the canvas plot space and
+ back. Both returns values that are offset with the plot offset.
+ Check the Flot source code for the complete set of attributes (or
+ output an axis with console.log() and inspect it).
+
+ With multiple axes, the extra axes are returned as x2axis, x3axis,
+ etc., e.g. getAxes().y2axis is the second y axis. You can check
+ y2axis.used to see whether the axis is associated with any data
+ points and y2axis.show to see if it is currently shown.
+
+ - getPlaceholder()
+
+ Returns placeholder that the plot was put into. This can be useful
+ for plugins for adding DOM elements or firing events.
+
+ - getCanvas()
+
+ Returns the canvas used for drawing in case you need to hack on it
+ yourself. You'll probably need to get the plot offset too.
+
+ - getPlotOffset()
+
+ Gets the offset that the grid has within the canvas as an object
+ with distances from the canvas edges as "left", "right", "top",
+ "bottom". I.e., if you draw a circle on the canvas with the center
+ placed at (left, top), its center will be at the top-most, left
+ corner of the grid.
+
+ - getOptions()
+
+ Gets the options for the plot, normalized, with default values
+ filled in. You get a reference to actual values used by Flot, so
+ if you modify the values in here, Flot will use the new values.
+ If you change something, you probably have to call draw() or
+ setupGrid() or triggerRedrawOverlay() to see the change.
+
+
+Hooks
+=====
+
+In addition to the public methods, the Plot object also has some hooks
+that can be used to modify the plotting process. You can install a
+callback function at various points in the process, the function then
+gets access to the internal data structures in Flot.
+
+Here's an overview of the phases Flot goes through:
+
+ 1. Plugin initialization, parsing options
+
+ 2. Constructing the canvases used for drawing
+
+ 3. Set data: parsing data specification, calculating colors,
+ copying raw data points into internal format,
+ normalizing them, finding max/min for axis auto-scaling
+
+ 4. Grid setup: calculating axis spacing, ticks, inserting tick
+ labels, the legend
+
+ 5. Draw: drawing the grid, drawing each of the series in turn
+
+ 6. Setting up event handling for interactive features
+
+ 7. Responding to events, if any
+
+ 8. Shutdown: this mostly happens in case a plot is overwritten
+
+Each hook is simply a function which is put in the appropriate array.
+You can add them through the "hooks" option, and they are also available
+after the plot is constructed as the "hooks" attribute on the returned
+plot object, e.g.
+
+ // define a simple draw hook
+ function hellohook(plot, canvascontext) { alert("hello!"); };
+
+ // pass it in, in an array since we might want to specify several
+ var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } });
+
+ // we can now find it again in plot.hooks.draw[0] unless a plugin
+ // has added other hooks
+
+The available hooks are described below. All hook callbacks get the
+plot object as first parameter. You can find some examples of defined
+hooks in the plugins bundled with Flot.
+
+ - processOptions [phase 1]
+
+ function(plot, options)
+
+ Called after Flot has parsed and merged options. Useful in the
+ instance where customizations beyond simple merging of default
+ values is needed. A plugin might use it to detect that it has been
+ enabled and then turn on or off other options.
+
+
+ - processRawData [phase 3]
+
+ function(plot, series, data, datapoints)
+
+ Called before Flot copies and normalizes the raw data for the given
+ series. If the function fills in datapoints.points with normalized
+ points and sets datapoints.pointsize to the size of the points,
+ Flot will skip the copying/normalization step for this series.
+
+ In any case, you might be interested in setting datapoints.format,
+ an array of objects for specifying how a point is normalized and
+ how it interferes with axis scaling.
+
+ The default format array for points is something along the lines of:
+
+ [
+ { x: true, number: true, required: true },
+ { y: true, number: true, required: true }
+ ]
+
+ The first object means that for the first coordinate it should be
+ taken into account when scaling the x axis, that it must be a
+ number, and that it is required - so if it is null or cannot be
+ converted to a number, the whole point will be zeroed out with
+ nulls. Beyond these you can also specify "defaultValue", a value to
+ use if the coordinate is null. This is for instance handy for bars
+ where one can omit the third coordinate (the bottom of the bar)
+ which then defaults to 0.
+
+
+ - processDatapoints [phase 3]
+
+ function(plot, series, datapoints)
+
+ Called after normalization of the given series but before finding
+ min/max of the data points. This hook is useful for implementing data
+ transformations. "datapoints" contains the normalized data points in
+ a flat array as datapoints.points with the size of a single point
+ given in datapoints.pointsize. Here's a simple transform that
+ multiplies all y coordinates by 2:
+
+ function multiply(plot, series, datapoints) {
+ var points = datapoints.points, ps = datapoints.pointsize;
+ for (var i = 0; i < points.length; i += ps)
+ points[i + 1] *= 2;
+ }
+
+ Note that you must leave datapoints in a good condition as Flot
+ doesn't check it or do any normalization on it afterwards.
+
+
+ - drawSeries [phase 5]
+
+ function(plot, canvascontext, series)
+
+ Hook for custom drawing of a single series. Called just before the
+ standard drawing routine has been called in the loop that draws
+ each series.
+
+
+ - draw [phase 5]
+
+ function(plot, canvascontext)
+
+ Hook for drawing on the canvas. Called after the grid is drawn
+ (unless it's disabled or grid.aboveData is set) and the series have
+ been plotted (in case any points, lines or bars have been turned
+ on). For examples of how to draw things, look at the source code.
+
+
+ - bindEvents [phase 6]
+
+ function(plot, eventHolder)
+
+ Called after Flot has setup its event handlers. Should set any
+ necessary event handlers on eventHolder, a jQuery object with the
+ canvas, e.g.
+
+ function (plot, eventHolder) {
+ eventHolder.mousedown(function (e) {
+ alert("You pressed the mouse at " + e.pageX + " " + e.pageY);
+ });
+ }
+
+ Interesting events include click, mousemove, mouseup/down. You can
+ use all jQuery events. Usually, the event handlers will update the
+ state by drawing something (add a drawOverlay hook and call
+ triggerRedrawOverlay) or firing an externally visible event for
+ user code. See the crosshair plugin for an example.
+
+ Currently, eventHolder actually contains both the static canvas
+ used for the plot itself and the overlay canvas used for
+ interactive features because some versions of IE get the stacking
+ order wrong. The hook only gets one event, though (either for the
+ overlay or for the static canvas).
+
+ Note that custom plot events generated by Flot are not generated on
+ eventHolder, but on the div placeholder supplied as the first
+ argument to the plot call. You can get that with
+ plot.getPlaceholder() - that's probably also the one you should use
+ if you need to fire a custom event.
+
+
+ - drawOverlay [phase 7]
+
+ function (plot, canvascontext)
+
+ The drawOverlay hook is used for interactive things that need a
+ canvas to draw on. The model currently used by Flot works the way
+ that an extra overlay canvas is positioned on top of the static
+ canvas. This overlay is cleared and then completely redrawn
+ whenever something interesting happens. This hook is called when
+ the overlay canvas is to be redrawn.
+
+ "canvascontext" is the 2D context of the overlay canvas. You can
+ use this to draw things. You'll most likely need some of the
+ metrics computed by Flot, e.g. plot.width()/plot.height(). See the
+ crosshair plugin for an example.
+
+
+ - shutdown [phase 8]
+
+ function (plot, eventHolder)
+
+ Run when plot.shutdown() is called, which usually only happens in
+ case a plot is overwritten by a new plot. If you're writing a
+ plugin that adds extra DOM elements or event handlers, you should
+ add a callback to clean up after you. Take a look at the section in
+ PLUGINS.txt for more info.
+
+
+Plugins
+-------
+
+Plugins extend the functionality of Flot. To use a plugin, simply
+include its Javascript file after Flot in the HTML page.
+
+If you're worried about download size/latency, you can concatenate all
+the plugins you use, and Flot itself for that matter, into one big file
+(make sure you get the order right), then optionally run it through a
+Javascript minifier such as YUI Compressor.
+
+Here's a brief explanation of how the plugin plumbings work:
+
+Each plugin registers itself in the global array $.plot.plugins. When
+you make a new plot object with $.plot, Flot goes through this array
+calling the "init" function of each plugin and merging default options
+from the "option" attribute of the plugin. The init function gets a
+reference to the plot object created and uses this to register hooks
+and add new public methods if needed.
+
+See the PLUGINS.txt file for details on how to write a plugin. As the
+above description hints, it's actually pretty easy.
+
+
+Version number
+--------------
+
+The version number of Flot is available in $.plot.version.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/FAQ.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/FAQ.txt
new file mode 100644
index 0000000000..e02b761888
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/FAQ.txt
@@ -0,0 +1,76 @@
+Frequently asked questions
+--------------------------
+
+Q: How much data can Flot cope with?
+
+A: Flot will happily draw everything you send to it so the answer
+depends on the browser. The excanvas emulation used for IE (built with
+VML) makes IE by far the slowest browser so be sure to test with that
+if IE users are in your target group.
+
+1000 points is not a problem, but as soon as you start having more
+points than the pixel width, you should probably start thinking about
+downsampling/aggregation as this is near the resolution limit of the
+chart anyway. If you downsample server-side, you also save bandwidth.
+
+
+Q: Flot isn't working when I'm using JSON data as source!
+
+A: Actually, Flot loves JSON data, you just got the format wrong.
+Double check that you're not inputting strings instead of numbers,
+like [["0", "-2.13"], ["5", "4.3"]]. This is most common mistake, and
+the error might not show up immediately because Javascript can do some
+conversion automatically.
+
+
+Q: Can I export the graph?
+
+A: This is a limitation of the canvas technology. There's a hook in
+the canvas object for getting an image out, but you won't get the tick
+labels. And it's not likely to be supported by IE. At this point, your
+best bet is probably taking a screenshot, e.g. with PrtScn.
+
+
+Q: The bars are all tiny in time mode?
+
+A: It's not really possible to determine the bar width automatically.
+So you have to set the width with the barWidth option which is NOT in
+pixels, but in the units of the x axis (or the y axis for horizontal
+bars). For time mode that's milliseconds so the default value of 1
+makes the bars 1 millisecond wide.
+
+
+Q: Can I use Flot with libraries like Mootools or Prototype?
+
+A: Yes, Flot supports it out of the box and it's easy! Just use jQuery
+instead of $, e.g. call jQuery.plot instead of $.plot and use
+jQuery(something) instead of $(something). As a convenience, you can
+put in a DOM element for the graph placeholder where the examples and
+the API documentation are using jQuery objects.
+
+Depending on how you include jQuery, you may have to add one line of
+code to prevent jQuery from overwriting functions from the other
+libraries, see the documentation in jQuery ("Using jQuery with other
+libraries") for details.
+
+
+Q: Flot doesn't work with [insert name of Javascript UI framework]!
+
+A: The only non-standard thing used by Flot is the canvas tag;
+otherwise it is simply a series of absolute positioned divs within the
+placeholder tag you put in. If this is not working, it's probably
+because the framework you're using is doing something weird with the
+DOM, or you're using it the wrong way.
+
+A common problem is that there's display:none on a container until the
+user does something. Many tab widgets work this way, and there's
+nothing wrong with it - you just can't call Flot inside a display:none
+container as explained in the README so you need to hold off the Flot
+call until the container is actually displayed (or use
+visibility:hidden instead of display:none or move the container
+off-screen).
+
+If you find there's a specific thing we can do to Flot to help, feel
+free to submit a bug report. Otherwise, you're welcome to ask for help
+on the forum/mailing list, but please don't submit a bug report to
+Flot.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/LICENSE.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/LICENSE.txt
new file mode 100644
index 0000000000..07d5b2094d
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/LICENSE.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2007-2009 IOLA and Ole Laursen
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/Makefile b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/Makefile
new file mode 100644
index 0000000000..b300f1a476
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/Makefile
@@ -0,0 +1,9 @@
+# Makefile for generating minified files
+
+.PHONY: all
+
+# we cheat and process all .js files instead of an exhaustive list
+all: $(patsubst %.js,%.min.js,$(filter-out %.min.js,$(wildcard *.js)))
+
+%.min.js: %.js
+ yui-compressor $< -o $@
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/NEWS.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/NEWS.txt
new file mode 100644
index 0000000000..5f8e1a0c05
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/NEWS.txt
@@ -0,0 +1,508 @@
+Flot 0.7
+--------
+
+API changes:
+
+Multiple axes support. Code using dual axes should be changed from
+using x2axis/y2axis in the options to using an array (although
+backwards-compatibility hooks are in place). For instance,
+
+ {
+ xaxis: { ... }, x2axis: { ... },
+ yaxis: { ... }, y2axis: { ... }
+ }
+
+becomes
+
+ {
+ xaxes: [ { ... }, { ... } ],
+ yaxes: [ { ... }, { ... } ]
+ }
+
+Note that if you're just using one axis, continue to use the
+xaxis/yaxis directly (it now sets the default settings for the
+arrays). Plugins touching the axes must be ported to take the extra
+axes into account, check the source to see some examples.
+
+A related change is that the visibility of axes is now auto-detected.
+So if you were relying on an axis to show up even without any data in
+the chart, you now need to set the axis "show" option explicitly.
+
+"tickColor" on the grid options is now deprecated in favour of a
+corresponding option on the axes, so { grid: { tickColor: "#000" }}
+becomes { xaxis: { tickColor: "#000"}, yaxis: { tickColor: "#000"} },
+but if you just configure a base color Flot will now autogenerate a
+tick color by adding transparency. Backwards-compatibility hooks are
+in place.
+
+Final note: now that IE 9 is coming out with canvas support, you may
+want to adapt the excanvas include to skip loading it in IE 9 (the
+examples have been adapted thanks to Ryley Breiddal). An alternative
+to excanvas using Flash has also surfaced, if your graphs are slow in
+IE, you may want to give it a spin:
+
+ http://code.google.com/p/flashcanvas/
+
+
+Changes:
+
+- Support for specifying a bottom for each point for line charts when
+ filling them, this means that an arbitrary bottom can be used
+ instead of just the x axis (based on patches patiently provided by
+ Roman V. Prikhodchenko).
+- New fillbetween plugin that can compute a bottom for a series from
+ another series, useful for filling areas between lines (see new
+ example percentiles.html for a use case).
+- More predictable handling of gaps for the stacking plugin, now all
+ undefined ranges are skipped.
+- Stacking plugin can stack horizontal bar charts.
+- Navigate plugin now redraws the plot while panning instead of only
+ after the fact (can be disabled by setting the pan.frameRate option
+ to null), raised by lastthemy (issue 235).
+- Date formatter now accepts %0m and %0d to get a zero-padded month or
+ day (issue raised by Maximillian Dornseif).
+- Revamped internals to support an unlimited number of axes, not just
+ dual (sponsored by Flight Data Services,
+ www.flightdataservices.com).
+- New setting on axes, "tickLength", to control the size of ticks or
+ turn them off without turning off the labels.
+- Axis labels are now put in container divs with classes, for instance
+ labels in the x axes can be reached via ".xAxis .tickLabel".
+- Support for setting the color of an axis (sponsored by Flight Data
+ Services, www.flightdataservices.com).
+- Tick color is now auto-generated as the base color with some
+ transparency (unless you override it).
+- Support for aligning ticks in the axes with "alignTicksWithAxis" to
+ ensure that they appear next to each other rather than in between,
+ at the expense of possibly awkward tick steps (sponsored by Flight
+ Data Services, www.flightdataservices.com).
+- Support for customizing the point type through a callback when
+ plotting points and new symbol plugin with some predefined point
+ types (sponsored by Utility Data Corporation).
+- Resize plugin for automatically redrawing when the placeholder
+ changes size, e.g. on window resizes (sponsored by Novus Partners).
+ A resize() method has been added to plot object facilitate this.
+- Support Infinity/-Infinity for plotting asymptotes by hacking it
+ into +/-Number.MAX_VALUE (reported by rabaea.mircea).
+- Support for restricting navigate plugin to not pan/zoom an axis (based
+ on patch by kkaefer).
+- Support for providing the drag cursor for the navigate plugin as an
+ option (based on patch by Kelly T. Moore).
+- Options for controlling whether an axis is shown or not (suggestion
+ by Timo Tuominen) and whether to reserve space for it even if it
+ isn't shown.
+- New attribute $.plot.version with the Flot version as a string.
+- The version comment is now included in the minified jquery.flot.min.js.
+- New options.grid.minBorderMargin for adjusting the minimum margin
+ provided around the border (based on patch by corani, issue 188).
+- Refactor replot behaviour so Flot tries to reuse the existing
+ canvas, adding shutdown() methods to the plot (based on patch by
+ Ryley Breiddal, issue 269). This prevents a memory leak in Chrome
+ and hopefully makes replotting faster for those who are using $.plot
+ instead of .setData()/.draw(). Also update jQuery to 1.5.1 to
+ prevent IE leaks fixed in jQuery.
+- New real-time line chart example.
+
+- New hooks: drawSeries, shutdown
+
+Bug fixes:
+
+- Fixed problem with findNearbyItem and bars on top of each other
+ (reported by ragingchikn, issue 242).
+- Fixed problem with ticks and the border (based on patch from
+ ultimatehustler69, issue 236).
+- Fixed problem with plugins adding options to the series objects.
+- Fixed a problem introduced in 0.6 with specifying a gradient with {
+ brightness: x, opacity: y }.
+- Don't use $.browser.msie, check for getContext on the created canvas
+ element instead and try to use excanvas if it's not found (fixes IE
+ 9 compatibility).
+- highlight(s, index) was looking up the point in the original s.data
+ instead of in the computed datapoints array, which breaks with
+ plugins that modify the datapoints (such as the stacking plugin).
+ Issue 316 reported by curlypaul924.
+- More robust handling of axis from data passed in from getData()
+ (problem reported by Morgan).
+- Fixed problem with turning off bar outline (issue 253, fix by Jordi
+ Castells).
+- Check the selection passed into setSelection in the selection
+ plugin, to guard against errors when synchronizing plots (fix by Lau
+ Bech Lauritzen).
+- Fix bug in crosshair code with mouseout resetting the crosshair even
+ if it is locked (fix by Lau Bech Lauritzen and Banko Adam).
+- Fix bug with points plotting using line width from lines rather than
+ points.
+- Fix bug with passing non-array 0 data (for plugins that don't expect
+ arrays, patch by vpapp1).
+- Fix errors in JSON in examples so they work with jQuery 1.4.2
+ (fix reported by honestbleeps, issue 357).
+- Fix bug with tooltip in interacting.html, this makes the tooltip
+ much smoother (fix by bdkahn). Fix related bug inside highlighting
+ handler in Flot.
+- Use closure trick to make inline colorhelpers plugin respect
+ jQuery.noConflict(true), renaming the global jQuery object (reported
+ by Nick Stielau).
+- Listen for mouseleave events and fire a plothover event with empty
+ item when it occurs to drop highlights when the mouse leaves the
+ plot (reported by by outspirit).
+- Fix bug with using aboveData with a background (reported by
+ amitayd).
+- Fix possible excanvas leak (report and suggested fix by tom9729).
+- Fix bug with backwards compatibility for shadowSize = 0 (report and
+ suggested fix by aspinak).
+- Adapt examples to skip loading excanvas (fix by Ryley Breiddal).
+- Fix bug that prevent a simple f(x) = -x transform from working
+ correctly (fix by Mike, issue 263).
+- Fix bug in restoring cursor in navigate plugin (reported by Matteo
+ Gattanini, issue 395).
+- Fix bug in picking items when transform/inverseTransform is in use
+ (reported by Ofri Raviv, and patches and analysis by Jan and Tom
+ Paton, issue 334 and 467).
+- Fix problem with unaligned ticks and hover/click events caused by
+ padding on the placeholder by hardcoding the placeholder padding to
+ 0 (reported by adityadineshsaxena, Matt Sommer, Daniel Atos and some
+ other people, issue 301).
+- Update colorhelpers plugin to avoid dying when trying to parse an
+ invalid string (reported by cadavor, issue 483).
+
+
+Flot 0.6
+--------
+
+API changes:
+
+1. Selection support has been moved to a plugin. Thus if you're
+passing selection: { mode: something }, you MUST include the file
+jquery.flot.selection.js after jquery.flot.js. This reduces the size
+of base Flot and makes it easier to customize the selection as well as
+improving code clarity. The change is based on a patch from andershol.
+
+2. In the global options specified in the $.plot command,
+"lines", "points", "bars" and "shadowSize" have been moved to a
+sub-object called "series", i.e.
+
+ $.plot(placeholder, data, { lines: { show: true }})
+
+should be changed to
+
+ $.plot(placeholder, data, { series: { lines: { show: true }}})
+
+All future series-specific options will go into this sub-object to
+simplify plugin writing. Backward-compatibility code is in place, so
+old code should not break.
+
+3. "plothover" no longer provides the original data point, but instead
+a normalized one, since there may be no corresponding original point.
+
+4. Due to a bug in previous versions of jQuery, you now need at least
+jQuery 1.2.6. But if you can, try jQuery 1.3.2 as it got some
+improvements in event handling speed.
+
+
+Changes:
+
+- Added support for disabling interactivity for specific data series
+ (request from Ronald Schouten and Steve Upton).
+
+- Flot now calls $() on the placeholder and optional legend container
+ passed in so you can specify DOM elements or CSS expressions to make
+ it easier to use Flot with libraries like Prototype or Mootools or
+ through raw JSON from Ajax responses.
+
+- A new "plotselecting" event is now emitted while the user is making
+ a selection.
+
+- The "plothover" event is now emitted immediately instead of at most
+ 10 times per second, you'll have to put in a setTimeout yourself if
+ you're doing something really expensive on this event.
+
+- The built-in date formatter can now be accessed as
+ $.plot.formatDate(...) (suggestion by Matt Manela) and even
+ replaced.
+
+- Added "borderColor" option to the grid (patch from Amaury Chamayou
+ and patch from Mike R. Williamson).
+
+- Added support for gradient backgrounds for the grid, take a look at
+ the "setting options" example (based on patch from Amaury Chamayou,
+ issue 90).
+
+- Gradient bars (suggestion by stefpet).
+
+- Added a "plotunselected" event which is triggered when the selection
+ is removed, see "selection" example (suggestion by Meda Ugo);
+
+- The option legend.margin can now specify horizontal and vertical
+ margins independently (suggestion by someone who's annoyed).
+
+- Data passed into Flot is now copied to a new canonical format to
+ enable further processing before it hits the drawing routines. As a
+ side-effect, this should make Flot more robust in the face of bad
+ data (and fixes issue 112).
+
+- Step-wise charting: line charts have a new option "steps" that when
+ set to true connects the points with horizontal/vertical steps
+ instead of diagonal lines.
+
+- The legend labelFormatter now passes the series in addition to just
+ the label (suggestion by Vincent Lemeltier).
+
+- Horizontal bars (based on patch by Jason LeBrun).
+
+- Support for partial bars by specifying a third coordinate, i.e. they
+ don't have to start from the axis. This can be used to make stacked
+ bars.
+
+- New option to disable the (grid.show).
+
+- Added pointOffset method for converting a point in data space to an
+ offset within the placeholder.
+
+- Plugin system: register an init method in the $.flot.plugins array
+ to get started, see PLUGINS.txt for details on how to write plugins
+ (it's easy). There are also some extra methods to enable access to
+ internal state.
+
+- Hooks: you can register functions that are called while Flot is
+ crunching the data and doing the plot. This can be used to modify
+ Flot without changing the source, useful for writing plugins. Some
+ hooks are defined, more are likely to come.
+
+- Threshold plugin: you can set a threshold and a color, and the data
+ points below that threshold will then get the color. Useful for
+ marking data below 0, for instance.
+
+- Stack plugin: you can specify a stack key for each series to have
+ them summed. This is useful for drawing additive/cumulative graphs
+ with bars and (currently unfilled) lines.
+
+- Crosshairs plugin: trace the mouse position on the axes, enable with
+ crosshair: { mode: "x"} (see the new tracking example for a use).
+
+- Image plugin: plot prerendered images.
+
+- Navigation plugin for panning and zooming a plot.
+
+- More configurable grid.
+
+- Axis transformation support, useful for non-linear plots, e.g. log
+ axes and compressed time axes (like omitting weekends).
+
+- Support for twelve-hour date formatting (patch by Forrest Aldridge).
+
+- The color parsing code in Flot has been cleaned up and split out so
+ it's now available as a separate jQuery plugin. It's included inline
+ in the Flot source to make dependency managing easier. This also
+ makes it really easy to use the color helpers in Flot plugins.
+
+Bug fixes:
+
+- Fixed two corner-case bugs when drawing filled curves (report and
+ analysis by Joshua Varner).
+- Fix auto-adjustment code when setting min to 0 for an axis where the
+ dataset is completely flat on that axis (report by chovy).
+- Fixed a bug with passing in data from getData to setData when the
+ secondary axes are used (issue 65, reported by nperelman).
+- Fixed so that it is possible to turn lines off when no other chart
+ type is shown (based on problem reported by Glenn Vanderburg), and
+ fixed so that setting lineWidth to 0 also hides the shadow (based on
+ problem reported by Sergio Nunes).
+- Updated mousemove position expression to the latest from jQuery (bug
+ reported by meyuchas).
+- Use CSS borders instead of background in legend (fix printing issue 25
+ and 45).
+- Explicitly convert axis min/max to numbers.
+- Fixed a bug with drawing marking lines with different colors
+ (reported by Khurram).
+- Fixed a bug with returning y2 values in the selection event (fix
+ by exists, issue 75).
+- Only set position relative on placeholder if it hasn't already a
+ position different from static (reported by kyberneticist, issue 95).
+- Don't round markings to prevent sub-pixel problems (reported by Dan
+ Lipsitt).
+- Make the grid border act similarly to a regular CSS border, i.e.
+ prevent it from overlapping the plot itself. This also fixes a
+ problem with anti-aliasing when the width is 1 pixel (reported by
+ Anthony Ettinger).
+- Imported version 3 of excanvas and fixed two issues with the newer
+ version. Hopefully, this will make Flot work with IE8 (nudge by
+ Fabien Menager, further analysis by Booink, issue 133).
+- Changed the shadow code for lines to hopefully look a bit better
+ with vertical lines.
+- Round tick positions to avoid possible problems with fractions
+ (suggestion by Fred, issue 130).
+- Made the heuristic for determining how many ticks to aim for a bit
+ smarter.
+- Fix for uneven axis margins (report and patch by Paul Kienzle) and
+ snapping to ticks (concurrent report and patch by lifthrasiir).
+- Fixed bug with slicing in findNearbyItems (patch by zollman).
+- Make heuristic for x axis label widths more dynamic (patch by
+ rickinhethuis).
+- Make sure points on top take precedence when finding nearby points
+ when hovering (reported by didroe, issue 224).
+
+Flot 0.5
+--------
+
+Backwards API change summary: Timestamps are now in UTC. Also
+"selected" event -> becomes "plotselected" with new data, the
+parameters for setSelection are now different (but backwards
+compatibility hooks are in place), coloredAreas becomes markings with
+a new interface (but backwards compatibility hooks are in place).
+
+
+Interactivity: added a new "plothover" event and this and the
+"plotclick" event now returns the closest data item (based on patch by
+/david, patch by Mark Byers for bar support). See the revamped
+"interacting with the data" example for some hints on what you can do.
+
+Highlighting: you can now highlight points and datapoints are
+autohighlighted when you hover over them (if hovering is turned on).
+
+Support for dual axis has been added (based on patch by someone who's
+annoyed and /david). For each data series you can specify which axes
+it belongs to, and there are two more axes, x2axis and y2axis, to
+customize. This affects the "selected" event which has been renamed to
+"plotselected" and spews out { xaxis: { from: -10, to: 20 } ... },
+setSelection in which the parameters are on a new form (backwards
+compatible hooks are in place so old code shouldn't break) and
+markings (formerly coloredAreas).
+
+Timestamps in time mode are now displayed according to
+UTC instead of the time zone of the visitor. This affects the way the
+timestamps should be input; you'll probably have to offset the
+timestamps according to your local time zone. It also affects any
+custom date handling code (which basically now should use the
+equivalent UTC date mehods, e.g. .setUTCMonth() instead of
+.setMonth().
+
+Added support for specifying the size of tick labels (axis.labelWidth,
+axis.labelHeight). Useful for specifying a max label size to keep
+multiple plots aligned.
+
+Markings, previously coloredAreas, are now specified as ranges on the
+axes, like { xaxis: { from: 0, to: 10 }}. Furthermore with markings
+you can now draw horizontal/vertical lines by setting from and to to
+the same coordinate (idea from line support patch by by Ryan Funduk).
+
+The "fill" option can now be a number that specifies the opacity of
+the fill.
+
+You can now specify a coordinate as null (like [2, null]) and Flot
+will take the other coordinate into account when scaling the axes
+(based on patch by joebno).
+
+New option for bars "align". Set it to "center" to center the bars on
+the value they represent.
+
+setSelection now takes a second parameter which you can use to prevent
+the method from firing the "plotselected" handler.
+
+Using the "container" option in legend now overwrites the container
+element instead of just appending to it (fixes infinite legend bug,
+reported by several people, fix by Brad Dewey).
+
+Fixed a bug in calculating spacing around the plot (reported by
+timothytoe). Fixed a bug in finding max values for all-negative data
+sets. Prevent the possibility of eternal looping in tick calculations.
+Fixed a bug when borderWidth is set to 0 (reported by
+Rob/sanchothefat). Fixed a bug with drawing bars extending below 0
+(reported by James Hewitt, patch by Ryan Funduk). Fixed a
+bug with line widths of bars (reported by MikeM). Fixed a bug with
+'nw' and 'sw' legend positions. Improved the handling of axis
+auto-scaling with bars. Fixed a bug with multi-line x-axis tick
+labels (reported by Luca Ciano). IE-fix help by Savage Zhang.
+
+
+Flot 0.4
+--------
+
+API changes: deprecated axis.noTicks in favor of just specifying the
+number as axis.ticks. So "xaxis: { noTicks: 10 }" becomes
+"xaxis: { ticks: 10 }"
+
+Time series support. Specify axis.mode: "time", put in Javascript
+timestamps as data, and Flot will automatically spit out sensible
+ticks. Take a look at the two new examples. The format can be
+customized with axis.timeformat and axis.monthNames, or if that fails
+with axis.tickFormatter.
+
+Support for colored background areas via grid.coloredAreas. Specify an
+array of { x1, y1, x2, y2 } objects or a function that returns these
+given { xmin, xmax, ymin, ymax }.
+
+More members on the plot object (report by Chris Davies and others).
+"getData" for inspecting the assigned settings on data series (e.g.
+color) and "setData", "setupGrid" and "draw" for updating the contents
+without a total replot.
+
+The default number of ticks to aim for is now dependent on the size of
+the plot in pixels. Support for customizing tick interval sizes
+directly with axis.minTickSize and axis.tickSize.
+
+Cleaned up the automatic axis scaling algorithm and fixed how it
+interacts with ticks. Also fixed a couple of tick-related corner case
+bugs (one reported by mainstreetmark, another reported by timothytoe).
+
+The option axis.tickFormatter now takes a function with two
+parameters, the second parameter is an optional object with
+information about the axis. It has min, max, tickDecimals, tickSize.
+
+Added support for segmented lines (based on patch from Michael
+MacDonald) and for ignoring null and bad values (suggestion from Nick
+Konidaris and joshwaihi).
+
+Added support for changing the border width (joebno and safoo).
+Label colors can be changed via CSS by selecting the tickLabel class.
+
+Fixed a bug in handling single-item bar series (reported by Emil
+Filipov). Fixed erratic behaviour when interacting with the plot
+with IE 7 (reported by Lau Bech Lauritzen). Prevent IE/Safari text
+selection when selecting stuff on the canvas.
+
+
+
+Flot 0.3
+--------
+
+This is mostly a quick-fix release because jquery.js wasn't included
+in the previous zip/tarball.
+
+Support clicking on the plot. Turn it on with grid: { clickable: true },
+then you get a "plotclick" event on the graph placeholder with the
+position in units of the plot.
+
+Fixed a bug in dealing with data where min = max, thanks to Michael
+Messinides.
+
+Include jquery.js in the zip/tarball.
+
+
+Flot 0.2
+--------
+
+Added support for putting a background behind the default legend. The
+default is the partly transparent background color. Added
+backgroundColor and backgroundOpacity to the legend options to control
+this.
+
+The ticks options can now be a callback function that takes one
+parameter, an object with the attributes min and max. The function
+should return a ticks array.
+
+Added labelFormatter option in legend, useful for turning the legend
+labels into links.
+
+Fixed a couple of bugs.
+
+The API should now be fully documented.
+
+Patch from Guy Fraser to make parts of the code smaller.
+
+API changes: Moved labelMargin option to grid from x/yaxis.
+
+
+Flot 0.1
+--------
+
+First public release.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/PLUGINS.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/PLUGINS.txt
new file mode 100644
index 0000000000..af3d90be58
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/PLUGINS.txt
@@ -0,0 +1,137 @@
+Writing plugins
+---------------
+
+All you need to do to make a new plugin is creating an init function
+and a set of options (if needed), stuffing it into an object and
+putting it in the $.plot.plugins array. For example:
+
+ function myCoolPluginInit(plot) {
+ plot.coolstring = "Hello!";
+ };
+
+ $.plot.plugins.push({ init: myCoolPluginInit, options: { ... } });
+
+ // if $.plot is called, it will return a plot object with the
+ // attribute "coolstring"
+
+Now, given that the plugin might run in many different places, it's
+a good idea to avoid leaking names. The usual trick here is wrap the
+above lines in an anonymous function which is called immediately, like
+this: (function () { inner code ... })(). To make it even more robust
+in case $ is not bound to jQuery but some other Javascript library, we
+can write it as
+
+ (function ($) {
+ // plugin definition
+ // ...
+ })(jQuery);
+
+There's a complete example below, but you should also check out the
+plugins bundled with Flot.
+
+
+Complete example
+----------------
+
+Here is a simple debug plugin which alerts each of the series in the
+plot. It has a single option that control whether it is enabled and
+how much info to output:
+
+ (function ($) {
+ function init(plot) {
+ var debugLevel = 1;
+
+ function checkDebugEnabled(plot, options) {
+ if (options.debug) {
+ debugLevel = options.debug;
+
+ plot.hooks.processDatapoints.push(alertSeries);
+ }
+ }
+
+ function alertSeries(plot, series, datapoints) {
+ var msg = "series " + series.label;
+ if (debugLevel > 1)
+ msg += " with " + series.data.length + " points";
+ alert(msg);
+ }
+
+ plot.hooks.processOptions.push(checkDebugEnabled);
+ }
+
+ var options = { debug: 0 };
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: "simpledebug",
+ version: "0.1"
+ });
+ })(jQuery);
+
+We also define "name" and "version". It's not used by Flot, but might
+be helpful for other plugins in resolving dependencies.
+
+Put the above in a file named "jquery.flot.debug.js", include it in an
+HTML page and then it can be used with:
+
+ $.plot($("#placeholder"), [...], { debug: 2 });
+
+This simple plugin illustrates a couple of points:
+
+ - It uses the anonymous function trick to avoid name pollution.
+ - It can be enabled/disabled through an option.
+ - Variables in the init function can be used to store plot-specific
+ state between the hooks.
+
+The two last points are important because there may be multiple plots
+on the same page, and you'd want to make sure they are not mixed up.
+
+
+Shutting down a plugin
+----------------------
+
+Each plot object has a shutdown hook which is run when plot.shutdown()
+is called. This usually mostly happens in case another plot is made on
+top of an existing one.
+
+The purpose of the hook is to give you a chance to unbind any event
+handlers you've registered and remove any extra DOM things you've
+inserted.
+
+The problem with event handlers is that you can have registered a
+handler which is run in some point in the future, e.g. with
+setTimeout(). Meanwhile, the plot may have been shutdown and removed,
+but because your event handler is still referencing it, it can't be
+garbage collected yet, and worse, if your handler eventually runs, it
+may overwrite stuff on a completely different plot.
+
+
+Some hints on the options
+-------------------------
+
+Plugins should always support appropriate options to enable/disable
+them because the plugin user may have several plots on the same page
+where only one should use the plugin. In most cases it's probably a
+good idea if the plugin is turned off rather than on per default, just
+like most of the powerful features in Flot.
+
+If the plugin needs options that are specific to each series, like the
+points or lines options in core Flot, you can put them in "series" in
+the options object, e.g.
+
+ var options = {
+ series: {
+ downsample: {
+ algorithm: null,
+ maxpoints: 1000
+ }
+ }
+ }
+
+Then they will be copied by Flot into each series, providing default
+values in case none are specified.
+
+Think hard and long about naming the options. These names are going to
+be public API, and code is going to depend on them if the plugin is
+successful.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/README.txt b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/README.txt
new file mode 100644
index 0000000000..1e49787a05
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/README.txt
@@ -0,0 +1,90 @@
+About
+-----
+
+Flot is a Javascript plotting library for jQuery. Read more at the
+website:
+
+ http://code.google.com/p/flot/
+
+Take a look at the examples linked from above, they should give a good
+impression of what Flot can do and the source code of the examples is
+probably the fastest way to learn how to use Flot.
+
+
+Installation
+------------
+
+Just include the Javascript file after you've included jQuery.
+
+Generally, all browsers that support the HTML5 canvas tag are
+supported.
+
+For support for Internet Explorer < 9, you can use Excanvas, a canvas
+emulator; this is used in the examples bundled with Flot. You just
+include the excanvas script like this:
+
+
+
+If it's not working on your development IE 6.0, check that it has
+support for VML which Excanvas is relying on. It appears that some
+stripped down versions used for test environments on virtual machines
+lack the VML support.
+
+You can also try using Flashcanvas (see
+http://code.google.com/p/flashcanvas/), which uses Flash to do the
+emulation. Although Flash can be a bit slower to load than VML, if
+you've got a lot of points, the Flash version can be much faster
+overall. Flot contains some wrapper code for activating Excanvas which
+Flashcanvas is compatible with.
+
+You need at least jQuery 1.2.6, but try at least 1.3.2 for interactive
+charts because of performance improvements in event handling.
+
+
+Basic usage
+-----------
+
+Create a placeholder div to put the graph in:
+
+
+
+You need to set the width and height of this div, otherwise the plot
+library doesn't know how to scale the graph. You can do it inline like
+this:
+
+
+
+You can also do it with an external stylesheet. Make sure that the
+placeholder isn't within something with a display:none CSS property -
+in that case, Flot has trouble measuring label dimensions which
+results in garbled looks and might have trouble measuring the
+placeholder dimensions which is fatal (it'll throw an exception).
+
+Then when the div is ready in the DOM, which is usually on document
+ready, run the plot function:
+
+ $.plot($("#placeholder"), data, options);
+
+Here, data is an array of data series and options is an object with
+settings if you want to customize the plot. Take a look at the
+examples for some ideas of what to put in or look at the reference
+in the file "API.txt". Here's a quick example that'll draw a line from
+(0, 0) to (1, 1):
+
+ $.plot($("#placeholder"), [ [[0, 0], [1, 1]] ], { yaxis: { max: 1 } });
+
+The plot function immediately draws the chart and then returns a plot
+object with a couple of methods.
+
+
+What's with the name?
+---------------------
+
+First: it's pronounced with a short o, like "plot". Not like "flawed".
+
+So "Flot" rhymes with "plot".
+
+And if you look up "flot" in a Danish-to-English dictionary, some up
+the words that come up are "good-looking", "attractive", "stylish",
+"smart", "impressive", "extravagant". One of the main goals with Flot
+is pretty looks.
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.js
new file mode 100644
index 0000000000..c40d6f7014
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.js
@@ -0,0 +1,1427 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns only support repeat.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Filling very large shapes (above 5000 points) is buggy.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+ var abs = m.abs;
+ var sqrt = m.sqrt;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ /**
+ * This funtion is assigned to the elements as element.getContext().
+ * @this {HTMLElement}
+ * @return {CanvasRenderingContext2D_}
+ */
+ function getContext() {
+ return this.context_ ||
+ (this.context_ = new CanvasRenderingContext2D_(this));
+ }
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Binds a function to an object. The returned function will always use the
+ * passed in {@code obj} as {@code this}.
+ *
+ * Example:
+ *
+ * g = bind(f, obj, a, b)
+ * g(c, d) // will do f.call(obj, a, b, c, d)
+ *
+ * @param {Function} f The function to bind the object to
+ * @param {Object} obj The object that should act as this when the function
+ * is called
+ * @param {*} var_args Rest arguments that will be used as the initial
+ * arguments when the function is called
+ * @return {Function} A new function that has bound this
+ */
+ function bind(f, obj, var_args) {
+ var a = slice.call(arguments, 2);
+ return function() {
+ return f.apply(obj, a.concat(slice.call(arguments)));
+ };
+ }
+
+ function encodeHtmlAttribute(s) {
+ return String(s).replace(/&/g, '&').replace(/"/g, '"');
+ }
+
+ function addNamespacesAndStylesheet(doc) {
+ // create xmlns
+ if (!doc.namespaces['g_vml_']) {
+ doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+ '#default#VML');
+
+ }
+ if (!doc.namespaces['g_o_']) {
+ doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+ '#default#VML');
+ }
+
+ // Setup default CSS. Only add one style sheet per document
+ if (!doc.styleSheets['ex_canvas_']) {
+ var ss = doc.createStyleSheet();
+ ss.owningElement.id = 'ex_canvas_';
+ ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+ // default size is 300x150 in Gecko and Opera
+ 'text-align:left;width:300px;height:150px}';
+ }
+ }
+
+ // Add namespaces and stylesheet at startup.
+ addNamespacesAndStylesheet(document);
+
+ var G_vmlCanvasManager_ = {
+ init: function(opt_doc) {
+ if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+ var doc = opt_doc || document;
+ // Create a dummy element so that IE will allow canvas elements to be
+ // recognized.
+ doc.createElement('canvas');
+ doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+ }
+ },
+
+ init_: function(doc) {
+ // find all canvas elements
+ var els = doc.getElementsByTagName('canvas');
+ for (var i = 0; i < els.length; i++) {
+ this.initElement(els[i]);
+ }
+ },
+
+ /**
+ * Public initializes a canvas element so that it can be used as canvas
+ * element from now on. This is called automatically before the page is
+ * loaded but if you are creating elements using createElement you need to
+ * make sure this is called on the element.
+ * @param {HTMLElement} el The canvas element to initialize.
+ * @return {HTMLElement} the element that was created.
+ */
+ initElement: function(el) {
+ if (!el.getContext) {
+ el.getContext = getContext;
+
+ // Add namespaces and stylesheet to document of the element.
+ addNamespacesAndStylesheet(el.ownerDocument);
+
+ // Remove fallback content. There is no way to hide text nodes so we
+ // just remove all childNodes. We could hide all elements and remove
+ // text nodes but who really cares about the fallback content.
+ el.innerHTML = '';
+
+ // do not use inline function because that will leak memory
+ el.attachEvent('onpropertychange', onPropertyChange);
+ el.attachEvent('onresize', onResize);
+
+ var attrs = el.attributes;
+ if (attrs.width && attrs.width.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setWidth_(attrs.width.nodeValue);
+ el.style.width = attrs.width.nodeValue + 'px';
+ } else {
+ el.width = el.clientWidth;
+ }
+ if (attrs.height && attrs.height.specified) {
+ // TODO: use runtimeStyle and coordsize
+ // el.getContext().setHeight_(attrs.height.nodeValue);
+ el.style.height = attrs.height.nodeValue + 'px';
+ } else {
+ el.height = el.clientHeight;
+ }
+ //el.getContext().setCoordsize_()
+ }
+ return el;
+ }
+ };
+
+ function onPropertyChange(e) {
+ var el = e.srcElement;
+
+ switch (e.propertyName) {
+ case 'width':
+ el.getContext().clearRect();
+ el.style.width = el.attributes.width.nodeValue + 'px';
+ // In IE8 this does not trigger onresize.
+ el.firstChild.style.width = el.clientWidth + 'px';
+ break;
+ case 'height':
+ el.getContext().clearRect();
+ el.style.height = el.attributes.height.nodeValue + 'px';
+ el.firstChild.style.height = el.clientHeight + 'px';
+ break;
+ }
+ }
+
+ function onResize(e) {
+ var el = e.srcElement;
+ if (el.firstChild) {
+ el.firstChild.style.width = el.clientWidth + 'px';
+ el.firstChild.style.height = el.clientHeight + 'px';
+ }
+ }
+
+ G_vmlCanvasManager_.init();
+
+ // precompute "00" to "FF"
+ var decToHex = [];
+ for (var i = 0; i < 16; i++) {
+ for (var j = 0; j < 16; j++) {
+ decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
+ }
+ }
+
+ function createMatrixIdentity() {
+ return [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1]
+ ];
+ }
+
+ function matrixMultiply(m1, m2) {
+ var result = createMatrixIdentity();
+
+ for (var x = 0; x < 3; x++) {
+ for (var y = 0; y < 3; y++) {
+ var sum = 0;
+
+ for (var z = 0; z < 3; z++) {
+ sum += m1[x][z] * m2[z][y];
+ }
+
+ result[x][y] = sum;
+ }
+ }
+ return result;
+ }
+
+ function copyState(o1, o2) {
+ o2.fillStyle = o1.fillStyle;
+ o2.lineCap = o1.lineCap;
+ o2.lineJoin = o1.lineJoin;
+ o2.lineWidth = o1.lineWidth;
+ o2.miterLimit = o1.miterLimit;
+ o2.shadowBlur = o1.shadowBlur;
+ o2.shadowColor = o1.shadowColor;
+ o2.shadowOffsetX = o1.shadowOffsetX;
+ o2.shadowOffsetY = o1.shadowOffsetY;
+ o2.strokeStyle = o1.strokeStyle;
+ o2.globalAlpha = o1.globalAlpha;
+ o2.font = o1.font;
+ o2.textAlign = o1.textAlign;
+ o2.textBaseline = o1.textBaseline;
+ o2.arcScaleX_ = o1.arcScaleX_;
+ o2.arcScaleY_ = o1.arcScaleY_;
+ o2.lineScale_ = o1.lineScale_;
+ }
+
+ var colorData = {
+ aliceblue: '#F0F8FF',
+ antiquewhite: '#FAEBD7',
+ aquamarine: '#7FFFD4',
+ azure: '#F0FFFF',
+ beige: '#F5F5DC',
+ bisque: '#FFE4C4',
+ black: '#000000',
+ blanchedalmond: '#FFEBCD',
+ blueviolet: '#8A2BE2',
+ brown: '#A52A2A',
+ burlywood: '#DEB887',
+ cadetblue: '#5F9EA0',
+ chartreuse: '#7FFF00',
+ chocolate: '#D2691E',
+ coral: '#FF7F50',
+ cornflowerblue: '#6495ED',
+ cornsilk: '#FFF8DC',
+ crimson: '#DC143C',
+ cyan: '#00FFFF',
+ darkblue: '#00008B',
+ darkcyan: '#008B8B',
+ darkgoldenrod: '#B8860B',
+ darkgray: '#A9A9A9',
+ darkgreen: '#006400',
+ darkgrey: '#A9A9A9',
+ darkkhaki: '#BDB76B',
+ darkmagenta: '#8B008B',
+ darkolivegreen: '#556B2F',
+ darkorange: '#FF8C00',
+ darkorchid: '#9932CC',
+ darkred: '#8B0000',
+ darksalmon: '#E9967A',
+ darkseagreen: '#8FBC8F',
+ darkslateblue: '#483D8B',
+ darkslategray: '#2F4F4F',
+ darkslategrey: '#2F4F4F',
+ darkturquoise: '#00CED1',
+ darkviolet: '#9400D3',
+ deeppink: '#FF1493',
+ deepskyblue: '#00BFFF',
+ dimgray: '#696969',
+ dimgrey: '#696969',
+ dodgerblue: '#1E90FF',
+ firebrick: '#B22222',
+ floralwhite: '#FFFAF0',
+ forestgreen: '#228B22',
+ gainsboro: '#DCDCDC',
+ ghostwhite: '#F8F8FF',
+ gold: '#FFD700',
+ goldenrod: '#DAA520',
+ grey: '#808080',
+ greenyellow: '#ADFF2F',
+ honeydew: '#F0FFF0',
+ hotpink: '#FF69B4',
+ indianred: '#CD5C5C',
+ indigo: '#4B0082',
+ ivory: '#FFFFF0',
+ khaki: '#F0E68C',
+ lavender: '#E6E6FA',
+ lavenderblush: '#FFF0F5',
+ lawngreen: '#7CFC00',
+ lemonchiffon: '#FFFACD',
+ lightblue: '#ADD8E6',
+ lightcoral: '#F08080',
+ lightcyan: '#E0FFFF',
+ lightgoldenrodyellow: '#FAFAD2',
+ lightgreen: '#90EE90',
+ lightgrey: '#D3D3D3',
+ lightpink: '#FFB6C1',
+ lightsalmon: '#FFA07A',
+ lightseagreen: '#20B2AA',
+ lightskyblue: '#87CEFA',
+ lightslategray: '#778899',
+ lightslategrey: '#778899',
+ lightsteelblue: '#B0C4DE',
+ lightyellow: '#FFFFE0',
+ limegreen: '#32CD32',
+ linen: '#FAF0E6',
+ magenta: '#FF00FF',
+ mediumaquamarine: '#66CDAA',
+ mediumblue: '#0000CD',
+ mediumorchid: '#BA55D3',
+ mediumpurple: '#9370DB',
+ mediumseagreen: '#3CB371',
+ mediumslateblue: '#7B68EE',
+ mediumspringgreen: '#00FA9A',
+ mediumturquoise: '#48D1CC',
+ mediumvioletred: '#C71585',
+ midnightblue: '#191970',
+ mintcream: '#F5FFFA',
+ mistyrose: '#FFE4E1',
+ moccasin: '#FFE4B5',
+ navajowhite: '#FFDEAD',
+ oldlace: '#FDF5E6',
+ olivedrab: '#6B8E23',
+ orange: '#FFA500',
+ orangered: '#FF4500',
+ orchid: '#DA70D6',
+ palegoldenrod: '#EEE8AA',
+ palegreen: '#98FB98',
+ paleturquoise: '#AFEEEE',
+ palevioletred: '#DB7093',
+ papayawhip: '#FFEFD5',
+ peachpuff: '#FFDAB9',
+ peru: '#CD853F',
+ pink: '#FFC0CB',
+ plum: '#DDA0DD',
+ powderblue: '#B0E0E6',
+ rosybrown: '#BC8F8F',
+ royalblue: '#4169E1',
+ saddlebrown: '#8B4513',
+ salmon: '#FA8072',
+ sandybrown: '#F4A460',
+ seagreen: '#2E8B57',
+ seashell: '#FFF5EE',
+ sienna: '#A0522D',
+ skyblue: '#87CEEB',
+ slateblue: '#6A5ACD',
+ slategray: '#708090',
+ slategrey: '#708090',
+ snow: '#FFFAFA',
+ springgreen: '#00FF7F',
+ steelblue: '#4682B4',
+ tan: '#D2B48C',
+ thistle: '#D8BFD8',
+ tomato: '#FF6347',
+ turquoise: '#40E0D0',
+ violet: '#EE82EE',
+ wheat: '#F5DEB3',
+ whitesmoke: '#F5F5F5',
+ yellowgreen: '#9ACD32'
+ };
+
+
+ function getRgbHslContent(styleString) {
+ var start = styleString.indexOf('(', 3);
+ var end = styleString.indexOf(')', start + 1);
+ var parts = styleString.substring(start + 1, end).split(',');
+ // add alpha if needed
+ if (parts.length == 4 && styleString.substr(3, 1) == 'a') {
+ alpha = Number(parts[3]);
+ } else {
+ parts[3] = 1;
+ }
+ return parts;
+ }
+
+ function percent(s) {
+ return parseFloat(s) / 100;
+ }
+
+ function clamp(v, min, max) {
+ return Math.min(max, Math.max(min, v));
+ }
+
+ function hslToRgb(parts){
+ var r, g, b;
+ h = parseFloat(parts[0]) / 360 % 360;
+ if (h < 0)
+ h++;
+ s = clamp(percent(parts[1]), 0, 1);
+ l = clamp(percent(parts[2]), 0, 1);
+ if (s == 0) {
+ r = g = b = l; // achromatic
+ } else {
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hueToRgb(p, q, h + 1 / 3);
+ g = hueToRgb(p, q, h);
+ b = hueToRgb(p, q, h - 1 / 3);
+ }
+
+ return '#' + decToHex[Math.floor(r * 255)] +
+ decToHex[Math.floor(g * 255)] +
+ decToHex[Math.floor(b * 255)];
+ }
+
+ function hueToRgb(m1, m2, h) {
+ if (h < 0)
+ h++;
+ if (h > 1)
+ h--;
+
+ if (6 * h < 1)
+ return m1 + (m2 - m1) * 6 * h;
+ else if (2 * h < 1)
+ return m2;
+ else if (3 * h < 2)
+ return m1 + (m2 - m1) * (2 / 3 - h) * 6;
+ else
+ return m1;
+ }
+
+ function processStyle(styleString) {
+ var str, alpha = 1;
+
+ styleString = String(styleString);
+ if (styleString.charAt(0) == '#') {
+ str = styleString;
+ } else if (/^rgb/.test(styleString)) {
+ var parts = getRgbHslContent(styleString);
+ var str = '#', n;
+ for (var i = 0; i < 3; i++) {
+ if (parts[i].indexOf('%') != -1) {
+ n = Math.floor(percent(parts[i]) * 255);
+ } else {
+ n = Number(parts[i]);
+ }
+ str += decToHex[clamp(n, 0, 255)];
+ }
+ alpha = parts[3];
+ } else if (/^hsl/.test(styleString)) {
+ var parts = getRgbHslContent(styleString);
+ str = hslToRgb(parts);
+ alpha = parts[3];
+ } else {
+ str = colorData[styleString] || styleString;
+ }
+ return {color: str, alpha: alpha};
+ }
+
+ var DEFAULT_STYLE = {
+ style: 'normal',
+ variant: 'normal',
+ weight: 'normal',
+ size: 10,
+ family: 'sans-serif'
+ };
+
+ // Internal text style cache
+ var fontStyleCache = {};
+
+ function processFontStyle(styleString) {
+ if (fontStyleCache[styleString]) {
+ return fontStyleCache[styleString];
+ }
+
+ var el = document.createElement('div');
+ var style = el.style;
+ try {
+ style.font = styleString;
+ } catch (ex) {
+ // Ignore failures to set to invalid font.
+ }
+
+ return fontStyleCache[styleString] = {
+ style: style.fontStyle || DEFAULT_STYLE.style,
+ variant: style.fontVariant || DEFAULT_STYLE.variant,
+ weight: style.fontWeight || DEFAULT_STYLE.weight,
+ size: style.fontSize || DEFAULT_STYLE.size,
+ family: style.fontFamily || DEFAULT_STYLE.family
+ };
+ }
+
+ function getComputedStyle(style, element) {
+ var computedStyle = {};
+
+ for (var p in style) {
+ computedStyle[p] = style[p];
+ }
+
+ // Compute the size
+ var canvasFontSize = parseFloat(element.currentStyle.fontSize),
+ fontSize = parseFloat(style.size);
+
+ if (typeof style.size == 'number') {
+ computedStyle.size = style.size;
+ } else if (style.size.indexOf('px') != -1) {
+ computedStyle.size = fontSize;
+ } else if (style.size.indexOf('em') != -1) {
+ computedStyle.size = canvasFontSize * fontSize;
+ } else if(style.size.indexOf('%') != -1) {
+ computedStyle.size = (canvasFontSize / 100) * fontSize;
+ } else if (style.size.indexOf('pt') != -1) {
+ computedStyle.size = fontSize / .75;
+ } else {
+ computedStyle.size = canvasFontSize;
+ }
+
+ // Different scaling between normal text and VML text. This was found using
+ // trial and error to get the same size as non VML text.
+ computedStyle.size *= 0.981;
+
+ return computedStyle;
+ }
+
+ function buildStyle(style) {
+ return style.style + ' ' + style.variant + ' ' + style.weight + ' ' +
+ style.size + 'px ' + style.family;
+ }
+
+ function processLineCap(lineCap) {
+ switch (lineCap) {
+ case 'butt':
+ return 'flat';
+ case 'round':
+ return 'round';
+ case 'square':
+ default:
+ return 'square';
+ }
+ }
+
+ /**
+ * This class implements CanvasRenderingContext2D interface as described by
+ * the WHATWG.
+ * @param {HTMLElement} surfaceElement The element that the 2D context should
+ * be associated with
+ */
+ function CanvasRenderingContext2D_(surfaceElement) {
+ this.m_ = createMatrixIdentity();
+
+ this.mStack_ = [];
+ this.aStack_ = [];
+ this.currentPath_ = [];
+
+ // Canvas context properties
+ this.strokeStyle = '#000';
+ this.fillStyle = '#000';
+
+ this.lineWidth = 1;
+ this.lineJoin = 'miter';
+ this.lineCap = 'butt';
+ this.miterLimit = Z * 1;
+ this.globalAlpha = 1;
+ this.font = '10px sans-serif';
+ this.textAlign = 'left';
+ this.textBaseline = 'alphabetic';
+ this.canvas = surfaceElement;
+
+ var el = surfaceElement.ownerDocument.createElement('div');
+ el.style.width = surfaceElement.clientWidth + 'px';
+ el.style.height = surfaceElement.clientHeight + 'px';
+ el.style.overflow = 'hidden';
+ el.style.position = 'absolute';
+ surfaceElement.appendChild(el);
+
+ this.element_ = el;
+ this.arcScaleX_ = 1;
+ this.arcScaleY_ = 1;
+ this.lineScale_ = 1;
+ }
+
+ var contextPrototype = CanvasRenderingContext2D_.prototype;
+ contextPrototype.clearRect = function() {
+ if (this.textMeasureEl_) {
+ this.textMeasureEl_.removeNode(true);
+ this.textMeasureEl_ = null;
+ }
+ this.element_.innerHTML = '';
+ };
+
+ contextPrototype.beginPath = function() {
+ // TODO: Branch current matrix so that save/restore has no effect
+ // as per safari docs.
+ this.currentPath_ = [];
+ };
+
+ contextPrototype.moveTo = function(aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
+ this.currentX_ = p.x;
+ this.currentY_ = p.y;
+ };
+
+ contextPrototype.lineTo = function(aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
+
+ this.currentX_ = p.x;
+ this.currentY_ = p.y;
+ };
+
+ contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+ aCP2x, aCP2y,
+ aX, aY) {
+ var p = this.getCoords_(aX, aY);
+ var cp1 = this.getCoords_(aCP1x, aCP1y);
+ var cp2 = this.getCoords_(aCP2x, aCP2y);
+ bezierCurveTo(this, cp1, cp2, p);
+ };
+
+ // Helper function that takes the already fixed cordinates.
+ function bezierCurveTo(self, cp1, cp2, p) {
+ self.currentPath_.push({
+ type: 'bezierCurveTo',
+ cp1x: cp1.x,
+ cp1y: cp1.y,
+ cp2x: cp2.x,
+ cp2y: cp2.y,
+ x: p.x,
+ y: p.y
+ });
+ self.currentX_ = p.x;
+ self.currentY_ = p.y;
+ }
+
+ contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+ // the following is lifted almost directly from
+ // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+
+ var cp = this.getCoords_(aCPx, aCPy);
+ var p = this.getCoords_(aX, aY);
+
+ var cp1 = {
+ x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
+ y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
+ };
+ var cp2 = {
+ x: cp1.x + (p.x - this.currentX_) / 3.0,
+ y: cp1.y + (p.y - this.currentY_) / 3.0
+ };
+
+ bezierCurveTo(this, cp1, cp2, p);
+ };
+
+ contextPrototype.arc = function(aX, aY, aRadius,
+ aStartAngle, aEndAngle, aClockwise) {
+ aRadius *= Z;
+ var arcType = aClockwise ? 'at' : 'wa';
+
+ var xStart = aX + mc(aStartAngle) * aRadius - Z2;
+ var yStart = aY + ms(aStartAngle) * aRadius - Z2;
+
+ var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
+ var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
+
+ // IE won't render arches drawn counter clockwise if xStart == xEnd.
+ if (xStart == xEnd && !aClockwise) {
+ xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+ // that can be represented in binary
+ }
+
+ var p = this.getCoords_(aX, aY);
+ var pStart = this.getCoords_(xStart, yStart);
+ var pEnd = this.getCoords_(xEnd, yEnd);
+
+ this.currentPath_.push({type: arcType,
+ x: p.x,
+ y: p.y,
+ radius: aRadius,
+ xStart: pStart.x,
+ yStart: pStart.y,
+ xEnd: pEnd.x,
+ yEnd: pEnd.y});
+
+ };
+
+ contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ };
+
+ contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+ var oldPath = this.currentPath_;
+ this.beginPath();
+
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.stroke();
+
+ this.currentPath_ = oldPath;
+ };
+
+ contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+ var oldPath = this.currentPath_;
+ this.beginPath();
+
+ this.moveTo(aX, aY);
+ this.lineTo(aX + aWidth, aY);
+ this.lineTo(aX + aWidth, aY + aHeight);
+ this.lineTo(aX, aY + aHeight);
+ this.closePath();
+ this.fill();
+
+ this.currentPath_ = oldPath;
+ };
+
+ contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+ var gradient = new CanvasGradient_('gradient');
+ gradient.x0_ = aX0;
+ gradient.y0_ = aY0;
+ gradient.x1_ = aX1;
+ gradient.y1_ = aY1;
+ return gradient;
+ };
+
+ contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
+ aX1, aY1, aR1) {
+ var gradient = new CanvasGradient_('gradientradial');
+ gradient.x0_ = aX0;
+ gradient.y0_ = aY0;
+ gradient.r0_ = aR0;
+ gradient.x1_ = aX1;
+ gradient.y1_ = aY1;
+ gradient.r1_ = aR1;
+ return gradient;
+ };
+
+ contextPrototype.drawImage = function(image, var_args) {
+ var dx, dy, dw, dh, sx, sy, sw, sh;
+
+ // to find the original width we overide the width and height
+ var oldRuntimeWidth = image.runtimeStyle.width;
+ var oldRuntimeHeight = image.runtimeStyle.height;
+ image.runtimeStyle.width = 'auto';
+ image.runtimeStyle.height = 'auto';
+
+ // get the original size
+ var w = image.width;
+ var h = image.height;
+
+ // and remove overides
+ image.runtimeStyle.width = oldRuntimeWidth;
+ image.runtimeStyle.height = oldRuntimeHeight;
+
+ if (arguments.length == 3) {
+ dx = arguments[1];
+ dy = arguments[2];
+ sx = sy = 0;
+ sw = dw = w;
+ sh = dh = h;
+ } else if (arguments.length == 5) {
+ dx = arguments[1];
+ dy = arguments[2];
+ dw = arguments[3];
+ dh = arguments[4];
+ sx = sy = 0;
+ sw = w;
+ sh = h;
+ } else if (arguments.length == 9) {
+ sx = arguments[1];
+ sy = arguments[2];
+ sw = arguments[3];
+ sh = arguments[4];
+ dx = arguments[5];
+ dy = arguments[6];
+ dw = arguments[7];
+ dh = arguments[8];
+ } else {
+ throw Error('Invalid number of arguments');
+ }
+
+ var d = this.getCoords_(dx, dy);
+
+ var w2 = sw / 2;
+ var h2 = sh / 2;
+
+ var vmlStr = [];
+
+ var W = 10;
+ var H = 10;
+
+ // For some reason that I've now forgotten, using divs didn't work
+ vmlStr.push(' ' ,
+ ' ',
+ ' ');
+
+ this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join(''));
+ };
+
+ contextPrototype.stroke = function(aFill) {
+ var W = 10;
+ var H = 10;
+ // Divide the shape into chunks if it's too long because IE has a limit
+ // somewhere for how long a VML shape can be. This simple division does
+ // not work with fills, only strokes, unfortunately.
+ var chunkSize = 5000;
+
+ var min = {x: null, y: null};
+ var max = {x: null, y: null};
+
+ for (var j = 0; j < this.currentPath_.length; j += chunkSize) {
+ var lineStr = [];
+ var lineOpen = false;
+
+ lineStr.push('');
+
+ if (!aFill) {
+ appendStroke(this, lineStr);
+ } else {
+ appendFill(this, lineStr, min, max);
+ }
+
+ lineStr.push(' ');
+
+ this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+ }
+ };
+
+ function appendStroke(ctx, lineStr) {
+ var a = processStyle(ctx.strokeStyle);
+ var color = a.color;
+ var opacity = a.alpha * ctx.globalAlpha;
+ var lineWidth = ctx.lineScale_ * ctx.lineWidth;
+
+ // VML cannot correctly render a line if the width is less than 1px.
+ // In that case, we dilute the color to make the line look thinner.
+ if (lineWidth < 1) {
+ opacity *= lineWidth;
+ }
+
+ lineStr.push(
+ ' '
+ );
+ }
+
+ function appendFill(ctx, lineStr, min, max) {
+ var fillStyle = ctx.fillStyle;
+ var arcScaleX = ctx.arcScaleX_;
+ var arcScaleY = ctx.arcScaleY_;
+ var width = max.x - min.x;
+ var height = max.y - min.y;
+ if (fillStyle instanceof CanvasGradient_) {
+ // TODO: Gradients transformed with the transformation matrix.
+ var angle = 0;
+ var focus = {x: 0, y: 0};
+
+ // additional offset
+ var shift = 0;
+ // scale factor for offset
+ var expansion = 1;
+
+ if (fillStyle.type_ == 'gradient') {
+ var x0 = fillStyle.x0_ / arcScaleX;
+ var y0 = fillStyle.y0_ / arcScaleY;
+ var x1 = fillStyle.x1_ / arcScaleX;
+ var y1 = fillStyle.y1_ / arcScaleY;
+ var p0 = ctx.getCoords_(x0, y0);
+ var p1 = ctx.getCoords_(x1, y1);
+ var dx = p1.x - p0.x;
+ var dy = p1.y - p0.y;
+ angle = Math.atan2(dx, dy) * 180 / Math.PI;
+
+ // The angle should be a non-negative number.
+ if (angle < 0) {
+ angle += 360;
+ }
+
+ // Very small angles produce an unexpected result because they are
+ // converted to a scientific notation string.
+ if (angle < 1e-6) {
+ angle = 0;
+ }
+ } else {
+ var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_);
+ focus = {
+ x: (p0.x - min.x) / width,
+ y: (p0.y - min.y) / height
+ };
+
+ width /= arcScaleX * Z;
+ height /= arcScaleY * Z;
+ var dimension = m.max(width, height);
+ shift = 2 * fillStyle.r0_ / dimension;
+ expansion = 2 * fillStyle.r1_ / dimension - shift;
+ }
+
+ // We need to sort the color stops in ascending order by offset,
+ // otherwise IE won't interpret it correctly.
+ var stops = fillStyle.colors_;
+ stops.sort(function(cs1, cs2) {
+ return cs1.offset - cs2.offset;
+ });
+
+ var length = stops.length;
+ var color1 = stops[0].color;
+ var color2 = stops[length - 1].color;
+ var opacity1 = stops[0].alpha * ctx.globalAlpha;
+ var opacity2 = stops[length - 1].alpha * ctx.globalAlpha;
+
+ var colors = [];
+ for (var i = 0; i < length; i++) {
+ var stop = stops[i];
+ colors.push(stop.offset * expansion + shift + ' ' + stop.color);
+ }
+
+ // When colors attribute is used, the meanings of opacity and o:opacity2
+ // are reversed.
+ lineStr.push(' ');
+ } else if (fillStyle instanceof CanvasPattern_) {
+ if (width && height) {
+ var deltaLeft = -min.x;
+ var deltaTop = -min.y;
+ lineStr.push(' ');
+ }
+ } else {
+ var a = processStyle(ctx.fillStyle);
+ var color = a.color;
+ var opacity = a.alpha * ctx.globalAlpha;
+ lineStr.push(' ');
+ }
+ }
+
+ contextPrototype.fill = function() {
+ this.stroke(true);
+ };
+
+ contextPrototype.closePath = function() {
+ this.currentPath_.push({type: 'close'});
+ };
+
+ /**
+ * @private
+ */
+ contextPrototype.getCoords_ = function(aX, aY) {
+ var m = this.m_;
+ return {
+ x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
+ y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
+ };
+ };
+
+ contextPrototype.save = function() {
+ var o = {};
+ copyState(this, o);
+ this.aStack_.push(o);
+ this.mStack_.push(this.m_);
+ this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+ };
+
+ contextPrototype.restore = function() {
+ if (this.aStack_.length) {
+ copyState(this.aStack_.pop(), this);
+ this.m_ = this.mStack_.pop();
+ }
+ };
+
+ function matrixIsFinite(m) {
+ return isFinite(m[0][0]) && isFinite(m[0][1]) &&
+ isFinite(m[1][0]) && isFinite(m[1][1]) &&
+ isFinite(m[2][0]) && isFinite(m[2][1]);
+ }
+
+ function setM(ctx, m, updateLineScale) {
+ if (!matrixIsFinite(m)) {
+ return;
+ }
+ ctx.m_ = m;
+
+ if (updateLineScale) {
+ // Get the line scale.
+ // Determinant of this.m_ means how much the area is enlarged by the
+ // transformation. So its square root can be used as a scale factor
+ // for width.
+ var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
+ ctx.lineScale_ = sqrt(abs(det));
+ }
+ }
+
+ contextPrototype.translate = function(aX, aY) {
+ var m1 = [
+ [1, 0, 0],
+ [0, 1, 0],
+ [aX, aY, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), false);
+ };
+
+ contextPrototype.rotate = function(aRot) {
+ var c = mc(aRot);
+ var s = ms(aRot);
+
+ var m1 = [
+ [c, s, 0],
+ [-s, c, 0],
+ [0, 0, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), false);
+ };
+
+ contextPrototype.scale = function(aX, aY) {
+ this.arcScaleX_ *= aX;
+ this.arcScaleY_ *= aY;
+ var m1 = [
+ [aX, 0, 0],
+ [0, aY, 0],
+ [0, 0, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), true);
+ };
+
+ contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) {
+ var m1 = [
+ [m11, m12, 0],
+ [m21, m22, 0],
+ [dx, dy, 1]
+ ];
+
+ setM(this, matrixMultiply(m1, this.m_), true);
+ };
+
+ contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) {
+ var m = [
+ [m11, m12, 0],
+ [m21, m22, 0],
+ [dx, dy, 1]
+ ];
+
+ setM(this, m, true);
+ };
+
+ /**
+ * The text drawing function.
+ * The maxWidth argument isn't taken in account, since no browser supports
+ * it yet.
+ */
+ contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) {
+ var m = this.m_,
+ delta = 1000,
+ left = 0,
+ right = delta,
+ offset = {x: 0, y: 0},
+ lineStr = [];
+
+ var fontStyle = getComputedStyle(processFontStyle(this.font),
+ this.element_);
+
+ var fontStyleString = buildStyle(fontStyle);
+
+ var elementStyle = this.element_.currentStyle;
+ var textAlign = this.textAlign.toLowerCase();
+ switch (textAlign) {
+ case 'left':
+ case 'center':
+ case 'right':
+ break;
+ case 'end':
+ textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left';
+ break;
+ case 'start':
+ textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left';
+ break;
+ default:
+ textAlign = 'left';
+ }
+
+ // 1.75 is an arbitrary number, as there is no info about the text baseline
+ switch (this.textBaseline) {
+ case 'hanging':
+ case 'top':
+ offset.y = fontStyle.size / 1.75;
+ break;
+ case 'middle':
+ break;
+ default:
+ case null:
+ case 'alphabetic':
+ case 'ideographic':
+ case 'bottom':
+ offset.y = -fontStyle.size / 2.25;
+ break;
+ }
+
+ switch(textAlign) {
+ case 'right':
+ left = delta;
+ right = 0.05;
+ break;
+ case 'center':
+ left = right = delta / 2;
+ break;
+ }
+
+ var d = this.getCoords_(x + offset.x, y + offset.y);
+
+ lineStr.push('');
+
+ if (stroke) {
+ appendStroke(this, lineStr);
+ } else {
+ // TODO: Fix the min and max params.
+ appendFill(this, lineStr, {x: -left, y: 0},
+ {x: right, y: fontStyle.size});
+ }
+
+ var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' +
+ m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0';
+
+ var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z);
+
+ lineStr.push(' ',
+ ' ',
+ ' ');
+
+ this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+ };
+
+ contextPrototype.fillText = function(text, x, y, maxWidth) {
+ this.drawText_(text, x, y, maxWidth, false);
+ };
+
+ contextPrototype.strokeText = function(text, x, y, maxWidth) {
+ this.drawText_(text, x, y, maxWidth, true);
+ };
+
+ contextPrototype.measureText = function(text) {
+ if (!this.textMeasureEl_) {
+ var s = ' ';
+ this.element_.insertAdjacentHTML('beforeEnd', s);
+ this.textMeasureEl_ = this.element_.lastChild;
+ }
+ var doc = this.element_.ownerDocument;
+ this.textMeasureEl_.innerHTML = '';
+ this.textMeasureEl_.style.font = this.font;
+ // Don't use innerHTML or innerText because they allow markup/whitespace.
+ this.textMeasureEl_.appendChild(doc.createTextNode(text));
+ return {width: this.textMeasureEl_.offsetWidth};
+ };
+
+ /******** STUBS ********/
+ contextPrototype.clip = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.arcTo = function() {
+ // TODO: Implement
+ };
+
+ contextPrototype.createPattern = function(image, repetition) {
+ return new CanvasPattern_(image, repetition);
+ };
+
+ // Gradient / Pattern Stubs
+ function CanvasGradient_(aType) {
+ this.type_ = aType;
+ this.x0_ = 0;
+ this.y0_ = 0;
+ this.r0_ = 0;
+ this.x1_ = 0;
+ this.y1_ = 0;
+ this.r1_ = 0;
+ this.colors_ = [];
+ }
+
+ CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+ aColor = processStyle(aColor);
+ this.colors_.push({offset: aOffset,
+ color: aColor.color,
+ alpha: aColor.alpha});
+ };
+
+ function CanvasPattern_(image, repetition) {
+ assertImageIsValid(image);
+ switch (repetition) {
+ case 'repeat':
+ case null:
+ case '':
+ this.repetition_ = 'repeat';
+ break
+ case 'repeat-x':
+ case 'repeat-y':
+ case 'no-repeat':
+ this.repetition_ = repetition;
+ break;
+ default:
+ throwException('SYNTAX_ERR');
+ }
+
+ this.src_ = image.src;
+ this.width_ = image.width;
+ this.height_ = image.height;
+ }
+
+ function throwException(s) {
+ throw new DOMException_(s);
+ }
+
+ function assertImageIsValid(img) {
+ if (!img || img.nodeType != 1 || img.tagName != 'IMG') {
+ throwException('TYPE_MISMATCH_ERR');
+ }
+ if (img.readyState != 'complete') {
+ throwException('INVALID_STATE_ERR');
+ }
+ }
+
+ function DOMException_(s) {
+ this.code = this[s];
+ this.message = s +': DOM Exception ' + this.code;
+ }
+ var p = DOMException_.prototype = new Error;
+ p.INDEX_SIZE_ERR = 1;
+ p.DOMSTRING_SIZE_ERR = 2;
+ p.HIERARCHY_REQUEST_ERR = 3;
+ p.WRONG_DOCUMENT_ERR = 4;
+ p.INVALID_CHARACTER_ERR = 5;
+ p.NO_DATA_ALLOWED_ERR = 6;
+ p.NO_MODIFICATION_ALLOWED_ERR = 7;
+ p.NOT_FOUND_ERR = 8;
+ p.NOT_SUPPORTED_ERR = 9;
+ p.INUSE_ATTRIBUTE_ERR = 10;
+ p.INVALID_STATE_ERR = 11;
+ p.SYNTAX_ERR = 12;
+ p.INVALID_MODIFICATION_ERR = 13;
+ p.NAMESPACE_ERR = 14;
+ p.INVALID_ACCESS_ERR = 15;
+ p.VALIDATION_ERR = 16;
+ p.TYPE_MISMATCH_ERR = 17;
+
+ // set up externs
+ G_vmlCanvasManager = G_vmlCanvasManager_;
+ CanvasRenderingContext2D = CanvasRenderingContext2D_;
+ CanvasGradient = CanvasGradient_;
+ CanvasPattern = CanvasPattern_;
+ DOMException = DOMException_;
+})();
+
+} // if
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.min.js
new file mode 100644
index 0000000000..12c74f7bea
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/excanvas.min.js
@@ -0,0 +1 @@
+if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" ',' "," ");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AHAL.x){AL.x=Z.x}if(AG.y==null||Z.yAL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push(" ')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae ')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push(" ')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push(' ')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push(' ',' ',' ');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z=' ';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()};
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.js
new file mode 100644
index 0000000000..d3524d786f
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.js
@@ -0,0 +1,179 @@
+/* Plugin for jQuery for working with colors.
+ *
+ * Version 1.1.
+ *
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ * var c = $.color.extract($("#mydiv"), 'background-color');
+ * console.log(c.r, c.g, c.b, c.a);
+ * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() return the same modified object
+ * instead of making a new one.
+ *
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
+ * produce a color rather than just crashing.
+ */
+
+(function($) {
+ $.color = {};
+
+ // construct color object with some convenient chainable helpers
+ $.color.make = function (r, g, b, a) {
+ var o = {};
+ o.r = r || 0;
+ o.g = g || 0;
+ o.b = b || 0;
+ o.a = a != null ? a : 1;
+
+ o.add = function (c, d) {
+ for (var i = 0; i < c.length; ++i)
+ o[c.charAt(i)] += d;
+ return o.normalize();
+ };
+
+ o.scale = function (c, f) {
+ for (var i = 0; i < c.length; ++i)
+ o[c.charAt(i)] *= f;
+ return o.normalize();
+ };
+
+ o.toString = function () {
+ if (o.a >= 1.0) {
+ return "rgb("+[o.r, o.g, o.b].join(",")+")";
+ } else {
+ return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")";
+ }
+ };
+
+ o.normalize = function () {
+ function clamp(min, value, max) {
+ return value < min ? min: (value > max ? max: value);
+ }
+
+ o.r = clamp(0, parseInt(o.r), 255);
+ o.g = clamp(0, parseInt(o.g), 255);
+ o.b = clamp(0, parseInt(o.b), 255);
+ o.a = clamp(0, o.a, 1);
+ return o;
+ };
+
+ o.clone = function () {
+ return $.color.make(o.r, o.b, o.g, o.a);
+ };
+
+ return o.normalize();
+ }
+
+ // extract CSS color property from element, going up in the DOM
+ // if it's "transparent"
+ $.color.extract = function (elem, css) {
+ var c;
+ do {
+ c = elem.css(css).toLowerCase();
+ // keep going until we find an element that has color, or
+ // we hit the body
+ if (c != '' && c != 'transparent')
+ break;
+ elem = elem.parent();
+ } while (!$.nodeName(elem.get(0), "body"));
+
+ // catch Safari's way of signalling transparent
+ if (c == "rgba(0, 0, 0, 0)")
+ c = "transparent";
+
+ return $.color.parse(c);
+ }
+
+ // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
+ // returns color object, if parsing failed, you get black (0, 0,
+ // 0) out
+ $.color.parse = function (str) {
+ var res, m = $.color.make;
+
+ // Look for rgb(num,num,num)
+ if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))
+ return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
+
+ // Look for rgba(num,num,num,num)
+ if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
+ return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
+
+ // Look for rgb(num%,num%,num%)
+ if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))
+ return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55);
+
+ // Look for rgba(num%,num%,num%,num)
+ if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
+ return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4]));
+
+ // Look for #a0b1c2
+ if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))
+ return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
+
+ // Look for #fff
+ if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))
+ return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16));
+
+ // Otherwise, we're most likely dealing with a named color
+ var name = $.trim(str).toLowerCase();
+ if (name == "transparent")
+ return m(255, 255, 255, 0);
+ else {
+ // default to black
+ res = lookupColors[name] || [0, 0, 0];
+ return m(res[0], res[1], res[2]);
+ }
+ }
+
+ var lookupColors = {
+ aqua:[0,255,255],
+ azure:[240,255,255],
+ beige:[245,245,220],
+ black:[0,0,0],
+ blue:[0,0,255],
+ brown:[165,42,42],
+ cyan:[0,255,255],
+ darkblue:[0,0,139],
+ darkcyan:[0,139,139],
+ darkgrey:[169,169,169],
+ darkgreen:[0,100,0],
+ darkkhaki:[189,183,107],
+ darkmagenta:[139,0,139],
+ darkolivegreen:[85,107,47],
+ darkorange:[255,140,0],
+ darkorchid:[153,50,204],
+ darkred:[139,0,0],
+ darksalmon:[233,150,122],
+ darkviolet:[148,0,211],
+ fuchsia:[255,0,255],
+ gold:[255,215,0],
+ green:[0,128,0],
+ indigo:[75,0,130],
+ khaki:[240,230,140],
+ lightblue:[173,216,230],
+ lightcyan:[224,255,255],
+ lightgreen:[144,238,144],
+ lightgrey:[211,211,211],
+ lightpink:[255,182,193],
+ lightyellow:[255,255,224],
+ lime:[0,255,0],
+ magenta:[255,0,255],
+ maroon:[128,0,0],
+ navy:[0,0,128],
+ olive:[128,128,0],
+ orange:[255,165,0],
+ pink:[255,192,203],
+ purple:[128,0,128],
+ violet:[128,0,128],
+ red:[255,0,0],
+ silver:[192,192,192],
+ white:[255,255,255],
+ yellow:[255,255,0]
+ };
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.min.js
new file mode 100644
index 0000000000..7f44c57b56
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.colorhelpers.min.js
@@ -0,0 +1 @@
+(function(b){b.color={};b.color.make=function(f,e,c,d){var h={};h.r=f||0;h.g=e||0;h.b=c||0;h.a=d!=null?d:1;h.add=function(k,j){for(var g=0;g=1){return"rgb("+[h.r,h.g,h.b].join(",")+")"}else{return"rgba("+[h.r,h.g,h.b,h.a].join(",")+")"}};h.normalize=function(){function g(j,k,i){return ki?i:k)}h.r=g(0,parseInt(h.r),255);h.g=g(0,parseInt(h.g),255);h.b=g(0,parseInt(h.b),255);h.a=g(0,h.a,1);return h};h.clone=function(){return b.color.make(h.r,h.b,h.g,h.a)};return h.normalize()};b.color.extract=function(e,d){var f;do{f=e.css(d).toLowerCase();if(f!=""&&f!="transparent"){break}e=e.parent()}while(!b.nodeName(e.get(0),"body"));if(f=="rgba(0, 0, 0, 0)"){f="transparent"}return b.color.parse(f)};b.color.parse=function(f){var e,c=b.color.make;if(e=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10))}if(e=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10),parseFloat(e[4]))}if(e=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55)}if(e=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55,parseFloat(e[4]))}if(e=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)){return c(parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16))}if(e=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)){return c(parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16))}var d=b.trim(f).toLowerCase();if(d=="transparent"){return c(255,255,255,0)}else{e=a[d]||[0,0,0];return c(e[0],e[1],e[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.js
new file mode 100644
index 0000000000..1d433f0074
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.js
@@ -0,0 +1,167 @@
+/*
+Flot plugin for showing crosshairs, thin lines, when the mouse hovers
+over the plot.
+
+ crosshair: {
+ mode: null or "x" or "y" or "xy"
+ color: color
+ lineWidth: number
+ }
+
+Set the mode to one of "x", "y" or "xy". The "x" mode enables a
+vertical crosshair that lets you trace the values on the x axis, "y"
+enables a horizontal crosshair and "xy" enables them both. "color" is
+the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
+"lineWidth" is the width of the drawn lines (default is 1).
+
+The plugin also adds four public methods:
+
+ - setCrosshair(pos)
+
+ Set the position of the crosshair. Note that this is cleared if
+ the user moves the mouse. "pos" is in coordinates of the plot and
+ should be on the form { x: xpos, y: ypos } (you can use x2/x3/...
+ if you're using multiple axes), which is coincidentally the same
+ format as what you get from a "plothover" event. If "pos" is null,
+ the crosshair is cleared.
+
+ - clearCrosshair()
+
+ Clear the crosshair.
+
+ - lockCrosshair(pos)
+
+ Cause the crosshair to lock to the current location, no longer
+ updating if the user moves the mouse. Optionally supply a position
+ (passed on to setCrosshair()) to move it to.
+
+ Example usage:
+ var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
+ $("#graph").bind("plothover", function (evt, position, item) {
+ if (item) {
+ // Lock the crosshair to the data point being hovered
+ myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
+ }
+ else {
+ // Return normal crosshair operation
+ myFlot.unlockCrosshair();
+ }
+ });
+
+ - unlockCrosshair()
+
+ Free the crosshair to move again after locking it.
+*/
+
+(function ($) {
+ var options = {
+ crosshair: {
+ mode: null, // one of null, "x", "y" or "xy",
+ color: "rgba(170, 0, 0, 0.80)",
+ lineWidth: 1
+ }
+ };
+
+ function init(plot) {
+ // position of crosshair in pixels
+ var crosshair = { x: -1, y: -1, locked: false };
+
+ plot.setCrosshair = function setCrosshair(pos) {
+ if (!pos)
+ crosshair.x = -1;
+ else {
+ var o = plot.p2c(pos);
+ crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
+ crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
+ }
+
+ plot.triggerRedrawOverlay();
+ };
+
+ plot.clearCrosshair = plot.setCrosshair; // passes null for pos
+
+ plot.lockCrosshair = function lockCrosshair(pos) {
+ if (pos)
+ plot.setCrosshair(pos);
+ crosshair.locked = true;
+ }
+
+ plot.unlockCrosshair = function unlockCrosshair() {
+ crosshair.locked = false;
+ }
+
+ function onMouseOut(e) {
+ if (crosshair.locked)
+ return;
+
+ if (crosshair.x != -1) {
+ crosshair.x = -1;
+ plot.triggerRedrawOverlay();
+ }
+ }
+
+ function onMouseMove(e) {
+ if (crosshair.locked)
+ return;
+
+ if (plot.getSelection && plot.getSelection()) {
+ crosshair.x = -1; // hide the crosshair while selecting
+ return;
+ }
+
+ var offset = plot.offset();
+ crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
+ crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
+ plot.triggerRedrawOverlay();
+ }
+
+ plot.hooks.bindEvents.push(function (plot, eventHolder) {
+ if (!plot.getOptions().crosshair.mode)
+ return;
+
+ eventHolder.mouseout(onMouseOut);
+ eventHolder.mousemove(onMouseMove);
+ });
+
+ plot.hooks.drawOverlay.push(function (plot, ctx) {
+ var c = plot.getOptions().crosshair;
+ if (!c.mode)
+ return;
+
+ var plotOffset = plot.getPlotOffset();
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ if (crosshair.x != -1) {
+ ctx.strokeStyle = c.color;
+ ctx.lineWidth = c.lineWidth;
+ ctx.lineJoin = "round";
+
+ ctx.beginPath();
+ if (c.mode.indexOf("x") != -1) {
+ ctx.moveTo(crosshair.x, 0);
+ ctx.lineTo(crosshair.x, plot.height());
+ }
+ if (c.mode.indexOf("y") != -1) {
+ ctx.moveTo(0, crosshair.y);
+ ctx.lineTo(plot.width(), crosshair.y);
+ }
+ ctx.stroke();
+ }
+ ctx.restore();
+ });
+
+ plot.hooks.shutdown.push(function (plot, eventHolder) {
+ eventHolder.unbind("mouseout", onMouseOut);
+ eventHolder.unbind("mousemove", onMouseMove);
+ });
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'crosshair',
+ version: '1.0'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.min.js
new file mode 100644
index 0000000000..ccaf240366
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.crosshair.min.js
@@ -0,0 +1 @@
+(function(b){var a={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function c(h){var j={x:-1,y:-1,locked:false};h.setCrosshair=function e(l){if(!l){j.x=-1}else{var k=h.p2c(l);j.x=Math.max(0,Math.min(k.left,h.width()));j.y=Math.max(0,Math.min(k.top,h.height()))}h.triggerRedrawOverlay()};h.clearCrosshair=h.setCrosshair;h.lockCrosshair=function f(k){if(k){h.setCrosshair(k)}j.locked=true};h.unlockCrosshair=function g(){j.locked=false};function d(k){if(j.locked){return}if(j.x!=-1){j.x=-1;h.triggerRedrawOverlay()}}function i(k){if(j.locked){return}if(h.getSelection&&h.getSelection()){j.x=-1;return}var l=h.offset();j.x=Math.max(0,Math.min(k.pageX-l.left,h.width()));j.y=Math.max(0,Math.min(k.pageY-l.top,h.height()));h.triggerRedrawOverlay()}h.hooks.bindEvents.push(function(l,k){if(!l.getOptions().crosshair.mode){return}k.mouseout(d);k.mousemove(i)});h.hooks.drawOverlay.push(function(m,k){var n=m.getOptions().crosshair;if(!n.mode){return}var l=m.getPlotOffset();k.save();k.translate(l.left,l.top);if(j.x!=-1){k.strokeStyle=n.color;k.lineWidth=n.lineWidth;k.lineJoin="round";k.beginPath();if(n.mode.indexOf("x")!=-1){k.moveTo(j.x,0);k.lineTo(j.x,m.height())}if(n.mode.indexOf("y")!=-1){k.moveTo(0,j.y);k.lineTo(m.width(),j.y)}k.stroke()}k.restore()});h.hooks.shutdown.push(function(l,k){k.unbind("mouseout",d);k.unbind("mousemove",i)})}b.plot.plugins.push({init:c,options:a,name:"crosshair",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.js
new file mode 100644
index 0000000000..69700e79ce
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.js
@@ -0,0 +1,183 @@
+/*
+Flot plugin for computing bottoms for filled line and bar charts.
+
+The case: you've got two series that you want to fill the area
+between. In Flot terms, you need to use one as the fill bottom of the
+other. You can specify the bottom of each data point as the third
+coordinate manually, or you can use this plugin to compute it for you.
+
+In order to name the other series, you need to give it an id, like this
+
+ var dataset = [
+ { data: [ ... ], id: "foo" } , // use default bottom
+ { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
+ ];
+
+ $.plot($("#placeholder"), dataset, { line: { show: true, fill: true }});
+
+As a convenience, if the id given is a number that doesn't appear as
+an id in the series, it is interpreted as the index in the array
+instead (so fillBetween: 0 can also mean the first series).
+
+Internally, the plugin modifies the datapoints in each series. For
+line series, extra data points might be inserted through
+interpolation. Note that at points where the bottom line is not
+defined (due to a null point or start/end of line), the current line
+will show a gap too. The algorithm comes from the jquery.flot.stack.js
+plugin, possibly some code could be shared.
+*/
+
+(function ($) {
+ var options = {
+ series: { fillBetween: null } // or number
+ };
+
+ function init(plot) {
+ function findBottomSeries(s, allseries) {
+ var i;
+ for (i = 0; i < allseries.length; ++i) {
+ if (allseries[i].id == s.fillBetween)
+ return allseries[i];
+ }
+
+ if (typeof s.fillBetween == "number") {
+ i = s.fillBetween;
+
+ if (i < 0 || i >= allseries.length)
+ return null;
+
+ return allseries[i];
+ }
+
+ return null;
+ }
+
+ function computeFillBottoms(plot, s, datapoints) {
+ if (s.fillBetween == null)
+ return;
+
+ var other = findBottomSeries(s, plot.getData());
+ if (!other)
+ return;
+
+ var ps = datapoints.pointsize,
+ points = datapoints.points,
+ otherps = other.datapoints.pointsize,
+ otherpoints = other.datapoints.points,
+ newpoints = [],
+ px, py, intery, qx, qy, bottom,
+ withlines = s.lines.show,
+ withbottom = ps > 2 && datapoints.format[2].y,
+ withsteps = withlines && s.lines.steps,
+ fromgap = true,
+ i = 0, j = 0, l;
+
+ while (true) {
+ if (i >= points.length)
+ break;
+
+ l = newpoints.length;
+
+ if (points[i] == null) {
+ // copy gaps
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ i += ps;
+ }
+ else if (j >= otherpoints.length) {
+ // for lines, we can't use the rest of the points
+ if (!withlines) {
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ }
+ i += ps;
+ }
+ else if (otherpoints[j] == null) {
+ // oops, got a gap
+ for (m = 0; m < ps; ++m)
+ newpoints.push(null);
+ fromgap = true;
+ j += otherps;
+ }
+ else {
+ // cases where we actually got two points
+ px = points[i];
+ py = points[i + 1];
+ qx = otherpoints[j];
+ qy = otherpoints[j + 1];
+ bottom = 0;
+
+ if (px == qx) {
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+
+ //newpoints[l + 1] += qy;
+ bottom = qy;
+
+ i += ps;
+ j += otherps;
+ }
+ else if (px > qx) {
+ // we got past point below, might need to
+ // insert interpolated extra point
+ if (withlines && i > 0 && points[i - ps] != null) {
+ intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
+ newpoints.push(qx);
+ newpoints.push(intery)
+ for (m = 2; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ bottom = qy;
+ }
+
+ j += otherps;
+ }
+ else { // px < qx
+ if (fromgap && withlines) {
+ // if we come from a gap, we just skip this point
+ i += ps;
+ continue;
+ }
+
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+
+ // we might be able to interpolate a point below,
+ // this can give us a better y
+ if (withlines && j > 0 && otherpoints[j - otherps] != null)
+ bottom = qy + (otherpoints[j - otherps + 1] - qy) * (px - qx) / (otherpoints[j - otherps] - qx);
+
+ //newpoints[l + 1] += bottom;
+
+ i += ps;
+ }
+
+ fromgap = false;
+
+ if (l != newpoints.length && withbottom)
+ newpoints[l + 2] = bottom;
+ }
+
+ // maintain the line steps invariant
+ if (withsteps && l != newpoints.length && l > 0
+ && newpoints[l] != null
+ && newpoints[l] != newpoints[l - ps]
+ && newpoints[l + 1] != newpoints[l - ps + 1]) {
+ for (m = 0; m < ps; ++m)
+ newpoints[l + ps + m] = newpoints[l + m];
+ newpoints[l + 1] = newpoints[l - ps + 1];
+ }
+ }
+
+ datapoints.points = newpoints;
+ }
+
+ plot.hooks.processDatapoints.push(computeFillBottoms);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'fillbetween',
+ version: '1.0'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.min.js
new file mode 100644
index 0000000000..47f3dfb6de
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.fillbetween.min.js
@@ -0,0 +1 @@
+(function(b){var a={series:{fillBetween:null}};function c(f){function d(j,h){var g;for(g=0;g=h.length){return null}return h[g]}return null}function e(B,u,g){if(u.fillBetween==null){return}var p=d(u,B.getData());if(!p){return}var y=g.pointsize,E=g.points,h=p.datapoints.pointsize,x=p.datapoints.points,r=[],w,v,k,G,F,q,t=u.lines.show,o=y>2&&g.format[2].y,n=t&&u.lines.steps,D=true,C=0,A=0,z;while(true){if(C>=E.length){break}z=r.length;if(E[C]==null){for(m=0;m=x.length){if(!t){for(m=0;mG){if(t&&C>0&&E[C-y]!=null){k=v+(E[C-y+1]-v)*(G-w)/(E[C-y]-w);r.push(G);r.push(k);for(m=2;m0&&x[A-h]!=null){q=F+(x[A-h+1]-F)*(w-G)/(x[A-h]-G)}C+=y}}D=false;if(z!=r.length&&o){r[z+2]=q}}}}if(n&&z!=r.length&&z>0&&r[z]!=null&&r[z]!=r[z-y]&&r[z+1]!=r[z-y+1]){for(m=0;m ').load(handler).error(handler).attr('src', url);
+ });
+ }
+
+ function drawSeries(plot, ctx, series) {
+ var plotOffset = plot.getPlotOffset();
+
+ if (!series.images || !series.images.show)
+ return;
+
+ var points = series.datapoints.points,
+ ps = series.datapoints.pointsize;
+
+ for (var i = 0; i < points.length; i += ps) {
+ var img = points[i],
+ x1 = points[i + 1], y1 = points[i + 2],
+ x2 = points[i + 3], y2 = points[i + 4],
+ xaxis = series.xaxis, yaxis = series.yaxis,
+ tmp;
+
+ // actually we should check img.complete, but it
+ // appears to be a somewhat unreliable indicator in
+ // IE6 (false even after load event)
+ if (!img || img.width <= 0 || img.height <= 0)
+ continue;
+
+ if (x1 > x2) {
+ tmp = x2;
+ x2 = x1;
+ x1 = tmp;
+ }
+ if (y1 > y2) {
+ tmp = y2;
+ y2 = y1;
+ y1 = tmp;
+ }
+
+ // if the anchor is at the center of the pixel, expand the
+ // image by 1/2 pixel in each direction
+ if (series.images.anchor == "center") {
+ tmp = 0.5 * (x2-x1) / (img.width - 1);
+ x1 -= tmp;
+ x2 += tmp;
+ tmp = 0.5 * (y2-y1) / (img.height - 1);
+ y1 -= tmp;
+ y2 += tmp;
+ }
+
+ // clip
+ if (x1 == x2 || y1 == y2 ||
+ x1 >= xaxis.max || x2 <= xaxis.min ||
+ y1 >= yaxis.max || y2 <= yaxis.min)
+ continue;
+
+ var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
+ if (x1 < xaxis.min) {
+ sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
+ x1 = xaxis.min;
+ }
+
+ if (x2 > xaxis.max) {
+ sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
+ x2 = xaxis.max;
+ }
+
+ if (y1 < yaxis.min) {
+ sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
+ y1 = yaxis.min;
+ }
+
+ if (y2 > yaxis.max) {
+ sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
+ y2 = yaxis.max;
+ }
+
+ x1 = xaxis.p2c(x1);
+ x2 = xaxis.p2c(x2);
+ y1 = yaxis.p2c(y1);
+ y2 = yaxis.p2c(y2);
+
+ // the transformation may have swapped us
+ if (x1 > x2) {
+ tmp = x2;
+ x2 = x1;
+ x1 = tmp;
+ }
+ if (y1 > y2) {
+ tmp = y2;
+ y2 = y1;
+ y1 = tmp;
+ }
+
+ tmp = ctx.globalAlpha;
+ ctx.globalAlpha *= series.images.alpha;
+ ctx.drawImage(img,
+ sx1, sy1, sx2 - sx1, sy2 - sy1,
+ x1 + plotOffset.left, y1 + plotOffset.top,
+ x2 - x1, y2 - y1);
+ ctx.globalAlpha = tmp;
+ }
+ }
+
+ function processRawData(plot, series, data, datapoints) {
+ if (!series.images.show)
+ return;
+
+ // format is Image, x1, y1, x2, y2 (opposite corners)
+ datapoints.format = [
+ { required: true },
+ { x: true, number: true, required: true },
+ { y: true, number: true, required: true },
+ { x: true, number: true, required: true },
+ { y: true, number: true, required: true }
+ ];
+ }
+
+ function init(plot) {
+ plot.hooks.processRawData.push(processRawData);
+ plot.hooks.drawSeries.push(drawSeries);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'image',
+ version: '1.1'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.image.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.image.min.js
new file mode 100644
index 0000000000..9480c1e7a3
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.image.min.js
@@ -0,0 +1 @@
+(function(c){var a={series:{images:{show:false,alpha:1,anchor:"corner"}}};c.plot.image={};c.plot.image.loadDataImages=function(g,f,k){var j=[],h=[];var i=f.series.images.show;c.each(g,function(l,m){if(!(i||m.images.show)){return}if(m.data){m=m.data}c.each(m,function(n,o){if(typeof o[0]=="string"){j.push(o[0]);h.push(o)}})});c.plot.image.load(j,function(l){c.each(h,function(n,o){var m=o[0];if(l[m]){o[0]=l[m]}});k()})};c.plot.image.load=function(h,i){var g=h.length,f={};if(g==0){i({})}c.each(h,function(k,j){var l=function(){--g;f[j]=this;if(g==0){i(f)}};c(" ").load(l).error(l).attr("src",j)})};function d(q,o,l){var m=q.getPlotOffset();if(!l.images||!l.images.show){return}var r=l.datapoints.points,n=l.datapoints.pointsize;for(var t=0;tv){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}if(l.images.anchor=="center"){x=0.5*(v-w)/(y.width-1);w-=x;v+=x;x=0.5*(f-g)/(y.height-1);g-=x;f+=x}if(w==v||g==f||w>=h.max||v<=h.min||g>=u.max||f<=u.min){continue}var k=0,s=0,j=y.width,p=y.height;if(wh.max){j+=(j-k)*(h.max-v)/(v-w);v=h.max}if(gu.max){s+=(s-p)*(u.max-f)/(f-g);f=u.max}w=h.p2c(w);v=h.p2c(v);g=u.p2c(g);f=u.p2c(f);if(w>v){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}x=o.globalAlpha;o.globalAlpha*=l.images.alpha;o.drawImage(y,k,s,j-k,p-s,w+m.left,g+m.top,v-w,f-g);o.globalAlpha=x}}function b(i,f,g,h){if(!f.images.show){return}h.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function e(f){f.hooks.processRawData.push(b);f.hooks.drawSeries.push(d)}c.plot.plugins.push({init:e,options:a,name:"image",version:"1.1"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.js
new file mode 100644
index 0000000000..aabc544e9a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.js
@@ -0,0 +1,2599 @@
+/*! Javascript plotting library for jQuery, v. 0.7.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ *
+ * Version 1.1.
+ *
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ * var c = $.color.extract($("#mydiv"), 'background-color');
+ * console.log(c.r, c.g, c.b, c.a);
+ * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() return the same modified object
+ * instead of making a new one.
+ *
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
+ * produce a color rather than just crashing.
+ */
+(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
+
+// the actual Flot code
+(function($) {
+ function Plot(placeholder, data_, options_, plugins) {
+ // data is on the form:
+ // [ series1, series2 ... ]
+ // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
+ // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
+
+ var series = [],
+ options = {
+ // the color theme used for graphs
+ colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
+ legend: {
+ show: true,
+ noColumns: 1, // number of colums in legend table
+ labelFormatter: null, // fn: string -> string
+ labelBoxBorderColor: "#ccc", // border color for the little label boxes
+ container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+ position: "ne", // position of default legend container within plot
+ margin: 5, // distance from grid edge to default legend container within plot
+ backgroundColor: null, // null means auto-detect
+ backgroundOpacity: 0.85 // set to 0 to avoid background
+ },
+ xaxis: {
+ show: null, // null = auto-detect, true = always, false = never
+ position: "bottom", // or "top"
+ mode: null, // null or "time"
+ color: null, // base color, labels, ticks
+ tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
+ transform: null, // null or f: number -> number to transform axis
+ inverseTransform: null, // if transform is set, this should be the inverse function
+ min: null, // min. value to show, null means set automatically
+ max: null, // max. value to show, null means set automatically
+ autoscaleMargin: null, // margin in % to add if auto-setting min/max
+ ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
+ tickFormatter: null, // fn: number -> string
+ labelWidth: null, // size of tick labels in pixels
+ labelHeight: null,
+ reserveSpace: null, // whether to reserve space even if axis isn't shown
+ tickLength: null, // size in pixels of ticks, or "full" for whole line
+ alignTicksWithAxis: null, // axis number or null for no sync
+
+ // mode specific options
+ tickDecimals: null, // no. of decimals, null means auto
+ tickSize: null, // number or [number, "unit"]
+ minTickSize: null, // number or [number, "unit"]
+ monthNames: null, // list of names of months
+ timeformat: null, // format string to use
+ twelveHourClock: false // 12 or 24 time in time mode
+ },
+ yaxis: {
+ autoscaleMargin: 0.02,
+ position: "left" // or "right"
+ },
+ xaxes: [],
+ yaxes: [],
+ series: {
+ points: {
+ show: false,
+ radius: 3,
+ lineWidth: 2, // in pixels
+ fill: true,
+ fillColor: "#ffffff",
+ symbol: "circle" // or callback
+ },
+ lines: {
+ // we don't put in show: false so we can see
+ // whether lines were actively disabled
+ lineWidth: 2, // in pixels
+ fill: false,
+ fillColor: null,
+ steps: false
+ },
+ bars: {
+ show: false,
+ lineWidth: 2, // in pixels
+ barWidth: 1, // in units of the x axis
+ fill: true,
+ fillColor: null,
+ align: "left", // or "center"
+ horizontal: false
+ },
+ shadowSize: 3
+ },
+ grid: {
+ show: true,
+ aboveData: false,
+ color: "#545454", // primary color used for outline and labels
+ backgroundColor: null, // null for transparent, else color
+ borderColor: null, // set if different from the grid color
+ tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
+ labelMargin: 5, // in pixels
+ axisMargin: 8, // in pixels
+ borderWidth: 2, // in pixels
+ minBorderMargin: null, // in pixels, null means taken from points radius
+ markings: null, // array of ranges or fn: axes -> array of ranges
+ markingsColor: "#f4f4f4",
+ markingsLineWidth: 2,
+ // interactive stuff
+ clickable: false,
+ hoverable: false,
+ autoHighlight: true, // highlight in case mouse is near
+ mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+ },
+ hooks: {}
+ },
+ canvas = null, // the canvas for the plot itself
+ overlay = null, // canvas for interactive stuff on top of plot
+ eventHolder = null, // jQuery object that events should be bound to
+ ctx = null, octx = null,
+ xaxes = [], yaxes = [],
+ plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
+ canvasWidth = 0, canvasHeight = 0,
+ plotWidth = 0, plotHeight = 0,
+ hooks = {
+ processOptions: [],
+ processRawData: [],
+ processDatapoints: [],
+ drawSeries: [],
+ draw: [],
+ bindEvents: [],
+ drawOverlay: [],
+ shutdown: []
+ },
+ plot = this;
+
+ // public functions
+ plot.setData = setData;
+ plot.setupGrid = setupGrid;
+ plot.draw = draw;
+ plot.getPlaceholder = function() { return placeholder; };
+ plot.getCanvas = function() { return canvas; };
+ plot.getPlotOffset = function() { return plotOffset; };
+ plot.width = function () { return plotWidth; };
+ plot.height = function () { return plotHeight; };
+ plot.offset = function () {
+ var o = eventHolder.offset();
+ o.left += plotOffset.left;
+ o.top += plotOffset.top;
+ return o;
+ };
+ plot.getData = function () { return series; };
+ plot.getAxes = function () {
+ var res = {}, i;
+ $.each(xaxes.concat(yaxes), function (_, axis) {
+ if (axis)
+ res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
+ });
+ return res;
+ };
+ plot.getXAxes = function () { return xaxes; };
+ plot.getYAxes = function () { return yaxes; };
+ plot.c2p = canvasToAxisCoords;
+ plot.p2c = axisToCanvasCoords;
+ plot.getOptions = function () { return options; };
+ plot.highlight = highlight;
+ plot.unhighlight = unhighlight;
+ plot.triggerRedrawOverlay = triggerRedrawOverlay;
+ plot.pointOffset = function(point) {
+ return {
+ left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
+ top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
+ };
+ };
+ plot.shutdown = shutdown;
+ plot.resize = function () {
+ getCanvasDimensions();
+ resizeCanvas(canvas);
+ resizeCanvas(overlay);
+ };
+
+ // public attributes
+ plot.hooks = hooks;
+
+ // initialize
+ initPlugins(plot);
+ parseOptions(options_);
+ setupCanvases();
+ setData(data_);
+ setupGrid();
+ draw();
+ bindEvents();
+
+
+ function executeHooks(hook, args) {
+ args = [plot].concat(args);
+ for (var i = 0; i < hook.length; ++i)
+ hook[i].apply(this, args);
+ }
+
+ function initPlugins() {
+ for (var i = 0; i < plugins.length; ++i) {
+ var p = plugins[i];
+ p.init(plot);
+ if (p.options)
+ $.extend(true, options, p.options);
+ }
+ }
+
+ function parseOptions(opts) {
+ var i;
+
+ $.extend(true, options, opts);
+
+ if (options.xaxis.color == null)
+ options.xaxis.color = options.grid.color;
+ if (options.yaxis.color == null)
+ options.yaxis.color = options.grid.color;
+
+ if (options.xaxis.tickColor == null) // backwards-compatibility
+ options.xaxis.tickColor = options.grid.tickColor;
+ if (options.yaxis.tickColor == null) // backwards-compatibility
+ options.yaxis.tickColor = options.grid.tickColor;
+
+ if (options.grid.borderColor == null)
+ options.grid.borderColor = options.grid.color;
+ if (options.grid.tickColor == null)
+ options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
+
+ // fill in defaults in axes, copy at least always the
+ // first as the rest of the code assumes it'll be there
+ for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
+ options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
+ for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
+ options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
+
+ // backwards compatibility, to be removed in future
+ if (options.xaxis.noTicks && options.xaxis.ticks == null)
+ options.xaxis.ticks = options.xaxis.noTicks;
+ if (options.yaxis.noTicks && options.yaxis.ticks == null)
+ options.yaxis.ticks = options.yaxis.noTicks;
+ if (options.x2axis) {
+ options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
+ options.xaxes[1].position = "top";
+ }
+ if (options.y2axis) {
+ options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
+ options.yaxes[1].position = "right";
+ }
+ if (options.grid.coloredAreas)
+ options.grid.markings = options.grid.coloredAreas;
+ if (options.grid.coloredAreasColor)
+ options.grid.markingsColor = options.grid.coloredAreasColor;
+ if (options.lines)
+ $.extend(true, options.series.lines, options.lines);
+ if (options.points)
+ $.extend(true, options.series.points, options.points);
+ if (options.bars)
+ $.extend(true, options.series.bars, options.bars);
+ if (options.shadowSize != null)
+ options.series.shadowSize = options.shadowSize;
+
+ // save options on axes for future reference
+ for (i = 0; i < options.xaxes.length; ++i)
+ getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
+ for (i = 0; i < options.yaxes.length; ++i)
+ getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
+
+ // add hooks from options
+ for (var n in hooks)
+ if (options.hooks[n] && options.hooks[n].length)
+ hooks[n] = hooks[n].concat(options.hooks[n]);
+
+ executeHooks(hooks.processOptions, [options]);
+ }
+
+ function setData(d) {
+ series = parseData(d);
+ fillInSeriesOptions();
+ processData();
+ }
+
+ function parseData(d) {
+ var res = [];
+ for (var i = 0; i < d.length; ++i) {
+ var s = $.extend(true, {}, options.series);
+
+ if (d[i].data != null) {
+ s.data = d[i].data; // move the data instead of deep-copy
+ delete d[i].data;
+
+ $.extend(true, s, d[i]);
+
+ d[i].data = s.data;
+ }
+ else
+ s.data = d[i];
+ res.push(s);
+ }
+
+ return res;
+ }
+
+ function axisNumber(obj, coord) {
+ var a = obj[coord + "axis"];
+ if (typeof a == "object") // if we got a real axis, extract number
+ a = a.n;
+ if (typeof a != "number")
+ a = 1; // default to first axis
+ return a;
+ }
+
+ function allAxes() {
+ // return flat array without annoying null entries
+ return $.grep(xaxes.concat(yaxes), function (a) { return a; });
+ }
+
+ function canvasToAxisCoords(pos) {
+ // return an object with x/y corresponding to all used axes
+ var res = {}, i, axis;
+ for (i = 0; i < xaxes.length; ++i) {
+ axis = xaxes[i];
+ if (axis && axis.used)
+ res["x" + axis.n] = axis.c2p(pos.left);
+ }
+
+ for (i = 0; i < yaxes.length; ++i) {
+ axis = yaxes[i];
+ if (axis && axis.used)
+ res["y" + axis.n] = axis.c2p(pos.top);
+ }
+
+ if (res.x1 !== undefined)
+ res.x = res.x1;
+ if (res.y1 !== undefined)
+ res.y = res.y1;
+
+ return res;
+ }
+
+ function axisToCanvasCoords(pos) {
+ // get canvas coords from the first pair of x/y found in pos
+ var res = {}, i, axis, key;
+
+ for (i = 0; i < xaxes.length; ++i) {
+ axis = xaxes[i];
+ if (axis && axis.used) {
+ key = "x" + axis.n;
+ if (pos[key] == null && axis.n == 1)
+ key = "x";
+
+ if (pos[key] != null) {
+ res.left = axis.p2c(pos[key]);
+ break;
+ }
+ }
+ }
+
+ for (i = 0; i < yaxes.length; ++i) {
+ axis = yaxes[i];
+ if (axis && axis.used) {
+ key = "y" + axis.n;
+ if (pos[key] == null && axis.n == 1)
+ key = "y";
+
+ if (pos[key] != null) {
+ res.top = axis.p2c(pos[key]);
+ break;
+ }
+ }
+ }
+
+ return res;
+ }
+
+ function getOrCreateAxis(axes, number) {
+ if (!axes[number - 1])
+ axes[number - 1] = {
+ n: number, // save the number for future reference
+ direction: axes == xaxes ? "x" : "y",
+ options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
+ };
+
+ return axes[number - 1];
+ }
+
+ function fillInSeriesOptions() {
+ var i;
+
+ // collect what we already got of colors
+ var neededColors = series.length,
+ usedColors = [],
+ assignedColors = [];
+ for (i = 0; i < series.length; ++i) {
+ var sc = series[i].color;
+ if (sc != null) {
+ --neededColors;
+ if (typeof sc == "number")
+ assignedColors.push(sc);
+ else
+ usedColors.push($.color.parse(series[i].color));
+ }
+ }
+
+ // we might need to generate more colors if higher indices
+ // are assigned
+ for (i = 0; i < assignedColors.length; ++i) {
+ neededColors = Math.max(neededColors, assignedColors[i] + 1);
+ }
+
+ // produce colors as needed
+ var colors = [], variation = 0;
+ i = 0;
+ while (colors.length < neededColors) {
+ var c;
+ if (options.colors.length == i) // check degenerate case
+ c = $.color.make(100, 100, 100);
+ else
+ c = $.color.parse(options.colors[i]);
+
+ // vary color if needed
+ var sign = variation % 2 == 1 ? -1 : 1;
+ c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
+
+ // FIXME: if we're getting to close to something else,
+ // we should probably skip this one
+ colors.push(c);
+
+ ++i;
+ if (i >= options.colors.length) {
+ i = 0;
+ ++variation;
+ }
+ }
+
+ // fill in the options
+ var colori = 0, s;
+ for (i = 0; i < series.length; ++i) {
+ s = series[i];
+
+ // assign colors
+ if (s.color == null) {
+ s.color = colors[colori].toString();
+ ++colori;
+ }
+ else if (typeof s.color == "number")
+ s.color = colors[s.color].toString();
+
+ // turn on lines automatically in case nothing is set
+ if (s.lines.show == null) {
+ var v, show = true;
+ for (v in s)
+ if (s[v] && s[v].show) {
+ show = false;
+ break;
+ }
+ if (show)
+ s.lines.show = true;
+ }
+
+ // setup axes
+ s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
+ s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
+ }
+ }
+
+ function processData() {
+ var topSentry = Number.POSITIVE_INFINITY,
+ bottomSentry = Number.NEGATIVE_INFINITY,
+ fakeInfinity = Number.MAX_VALUE,
+ i, j, k, m, length,
+ s, points, ps, x, y, axis, val, f, p;
+
+ function updateAxis(axis, min, max) {
+ if (min < axis.datamin && min != -fakeInfinity)
+ axis.datamin = min;
+ if (max > axis.datamax && max != fakeInfinity)
+ axis.datamax = max;
+ }
+
+ $.each(allAxes(), function (_, axis) {
+ // init axis
+ axis.datamin = topSentry;
+ axis.datamax = bottomSentry;
+ axis.used = false;
+ });
+
+ for (i = 0; i < series.length; ++i) {
+ s = series[i];
+ s.datapoints = { points: [] };
+
+ executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
+ }
+
+ // first pass: clean and copy data
+ for (i = 0; i < series.length; ++i) {
+ s = series[i];
+
+ var data = s.data, format = s.datapoints.format;
+
+ if (!format) {
+ format = [];
+ // find out how to copy
+ format.push({ x: true, number: true, required: true });
+ format.push({ y: true, number: true, required: true });
+
+ if (s.bars.show || (s.lines.show && s.lines.fill)) {
+ format.push({ y: true, number: true, required: false, defaultValue: 0 });
+ if (s.bars.horizontal) {
+ delete format[format.length - 1].y;
+ format[format.length - 1].x = true;
+ }
+ }
+
+ s.datapoints.format = format;
+ }
+
+ if (s.datapoints.pointsize != null)
+ continue; // already filled in
+
+ s.datapoints.pointsize = format.length;
+
+ ps = s.datapoints.pointsize;
+ points = s.datapoints.points;
+
+ insertSteps = s.lines.show && s.lines.steps;
+ s.xaxis.used = s.yaxis.used = true;
+
+ for (j = k = 0; j < data.length; ++j, k += ps) {
+ p = data[j];
+
+ var nullify = p == null;
+ if (!nullify) {
+ for (m = 0; m < ps; ++m) {
+ val = p[m];
+ f = format[m];
+
+ if (f) {
+ if (f.number && val != null) {
+ val = +val; // convert to number
+ if (isNaN(val))
+ val = null;
+ else if (val == Infinity)
+ val = fakeInfinity;
+ else if (val == -Infinity)
+ val = -fakeInfinity;
+ }
+
+ if (val == null) {
+ if (f.required)
+ nullify = true;
+
+ if (f.defaultValue != null)
+ val = f.defaultValue;
+ }
+ }
+
+ points[k + m] = val;
+ }
+ }
+
+ if (nullify) {
+ for (m = 0; m < ps; ++m) {
+ val = points[k + m];
+ if (val != null) {
+ f = format[m];
+ // extract min/max info
+ if (f.x)
+ updateAxis(s.xaxis, val, val);
+ if (f.y)
+ updateAxis(s.yaxis, val, val);
+ }
+ points[k + m] = null;
+ }
+ }
+ else {
+ // a little bit of line specific stuff that
+ // perhaps shouldn't be here, but lacking
+ // better means...
+ if (insertSteps && k > 0
+ && points[k - ps] != null
+ && points[k - ps] != points[k]
+ && points[k - ps + 1] != points[k + 1]) {
+ // copy the point to make room for a middle point
+ for (m = 0; m < ps; ++m)
+ points[k + ps + m] = points[k + m];
+
+ // middle point has same y
+ points[k + 1] = points[k - ps + 1];
+
+ // we've added a point, better reflect that
+ k += ps;
+ }
+ }
+ }
+ }
+
+ // give the hooks a chance to run
+ for (i = 0; i < series.length; ++i) {
+ s = series[i];
+
+ executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
+ }
+
+ // second pass: find datamax/datamin for auto-scaling
+ for (i = 0; i < series.length; ++i) {
+ s = series[i];
+ points = s.datapoints.points,
+ ps = s.datapoints.pointsize;
+
+ var xmin = topSentry, ymin = topSentry,
+ xmax = bottomSentry, ymax = bottomSentry;
+
+ for (j = 0; j < points.length; j += ps) {
+ if (points[j] == null)
+ continue;
+
+ for (m = 0; m < ps; ++m) {
+ val = points[j + m];
+ f = format[m];
+ if (!f || val == fakeInfinity || val == -fakeInfinity)
+ continue;
+
+ if (f.x) {
+ if (val < xmin)
+ xmin = val;
+ if (val > xmax)
+ xmax = val;
+ }
+ if (f.y) {
+ if (val < ymin)
+ ymin = val;
+ if (val > ymax)
+ ymax = val;
+ }
+ }
+ }
+
+ if (s.bars.show) {
+ // make sure we got room for the bar on the dancing floor
+ var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
+ if (s.bars.horizontal) {
+ ymin += delta;
+ ymax += delta + s.bars.barWidth;
+ }
+ else {
+ xmin += delta;
+ xmax += delta + s.bars.barWidth;
+ }
+ }
+
+ updateAxis(s.xaxis, xmin, xmax);
+ updateAxis(s.yaxis, ymin, ymax);
+ }
+
+ $.each(allAxes(), function (_, axis) {
+ if (axis.datamin == topSentry)
+ axis.datamin = null;
+ if (axis.datamax == bottomSentry)
+ axis.datamax = null;
+ });
+ }
+
+ function makeCanvas(skipPositioning, cls) {
+ var c = document.createElement('canvas');
+ c.className = cls;
+ c.width = canvasWidth;
+ c.height = canvasHeight;
+
+ if (!skipPositioning)
+ $(c).css({ position: 'absolute', left: 0, top: 0 });
+
+ $(c).appendTo(placeholder);
+
+ if (!c.getContext) // excanvas hack
+ c = window.G_vmlCanvasManager.initElement(c);
+
+ // used for resetting in case we get replotted
+ c.getContext("2d").save();
+
+ return c;
+ }
+
+ function getCanvasDimensions() {
+ canvasWidth = placeholder.width();
+ canvasHeight = placeholder.height();
+
+ if (canvasWidth <= 0 || canvasHeight <= 0)
+ throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
+ }
+
+ function resizeCanvas(c) {
+ // resizing should reset the state (excanvas seems to be
+ // buggy though)
+ if (c.width != canvasWidth)
+ c.width = canvasWidth;
+
+ if (c.height != canvasHeight)
+ c.height = canvasHeight;
+
+ // so try to get back to the initial state (even if it's
+ // gone now, this should be safe according to the spec)
+ var cctx = c.getContext("2d");
+ cctx.restore();
+
+ // and save again
+ cctx.save();
+ }
+
+ function setupCanvases() {
+ var reused,
+ existingCanvas = placeholder.children("canvas.base"),
+ existingOverlay = placeholder.children("canvas.overlay");
+
+ if (existingCanvas.length == 0 || existingOverlay == 0) {
+ // init everything
+
+ placeholder.html(""); // make sure placeholder is clear
+
+ placeholder.css({ padding: 0 }); // padding messes up the positioning
+
+ if (placeholder.css("position") == 'static')
+ placeholder.css("position", "relative"); // for positioning labels and overlay
+
+ getCanvasDimensions();
+
+ canvas = makeCanvas(true, "base");
+ overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
+
+ reused = false;
+ }
+ else {
+ // reuse existing elements
+
+ canvas = existingCanvas.get(0);
+ overlay = existingOverlay.get(0);
+
+ reused = true;
+ }
+
+ ctx = canvas.getContext("2d");
+ octx = overlay.getContext("2d");
+
+ // we include the canvas in the event holder too, because IE 7
+ // sometimes has trouble with the stacking order
+ eventHolder = $([overlay, canvas]);
+
+ if (reused) {
+ // run shutdown in the old plot object
+ placeholder.data("plot").shutdown();
+
+ // reset reused canvases
+ plot.resize();
+
+ // make sure overlay pixels are cleared (canvas is cleared when we redraw)
+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+ // then whack any remaining obvious garbage left
+ eventHolder.unbind();
+ placeholder.children().not([canvas, overlay]).remove();
+ }
+
+ // save in case we get replotted
+ placeholder.data("plot", plot);
+ }
+
+ function bindEvents() {
+ // bind events
+ if (options.grid.hoverable) {
+ eventHolder.mousemove(onMouseMove);
+ eventHolder.mouseleave(onMouseLeave);
+ }
+
+ if (options.grid.clickable)
+ eventHolder.click(onClick);
+
+ executeHooks(hooks.bindEvents, [eventHolder]);
+ }
+
+ function shutdown() {
+ if (redrawTimeout)
+ clearTimeout(redrawTimeout);
+
+ eventHolder.unbind("mousemove", onMouseMove);
+ eventHolder.unbind("mouseleave", onMouseLeave);
+ eventHolder.unbind("click", onClick);
+
+ executeHooks(hooks.shutdown, [eventHolder]);
+ }
+
+ function setTransformationHelpers(axis) {
+ // set helper functions on the axis, assumes plot area
+ // has been computed already
+
+ function identity(x) { return x; }
+
+ var s, m, t = axis.options.transform || identity,
+ it = axis.options.inverseTransform;
+
+ // precompute how much the axis is scaling a point
+ // in canvas space
+ if (axis.direction == "x") {
+ s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
+ m = Math.min(t(axis.max), t(axis.min));
+ }
+ else {
+ s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
+ s = -s;
+ m = Math.max(t(axis.max), t(axis.min));
+ }
+
+ // data point to canvas coordinate
+ if (t == identity) // slight optimization
+ axis.p2c = function (p) { return (p - m) * s; };
+ else
+ axis.p2c = function (p) { return (t(p) - m) * s; };
+ // canvas coordinate to data point
+ if (!it)
+ axis.c2p = function (c) { return m + c / s; };
+ else
+ axis.c2p = function (c) { return it(m + c / s); };
+ }
+
+ function measureTickLabels(axis) {
+ var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
+ l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
+
+ function makeDummyDiv(labels, width) {
+ return $('' +
+ '
'
+ + labels.join("") + '
')
+ .appendTo(placeholder);
+ }
+
+ if (axis.direction == "x") {
+ // to avoid measuring the widths of the labels (it's slow), we
+ // construct fixed-size boxes and put the labels inside
+ // them, we don't need the exact figures and the
+ // fixed-size box content is easy to center
+ if (w == null)
+ w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
+
+ // measure x label heights
+ if (h == null) {
+ labels = [];
+ for (i = 0; i < ticks.length; ++i) {
+ l = ticks[i].label;
+ if (l)
+ labels.push('' + l + '
');
+ }
+
+ if (labels.length > 0) {
+ // stick them all in the same div and measure
+ // collective height
+ labels.push('
');
+ dummyDiv = makeDummyDiv(labels, "width:10000px;");
+ h = dummyDiv.height();
+ dummyDiv.remove();
+ }
+ }
+ }
+ else if (w == null || h == null) {
+ // calculate y label dimensions
+ for (i = 0; i < ticks.length; ++i) {
+ l = ticks[i].label;
+ if (l)
+ labels.push('' + l + '
');
+ }
+
+ if (labels.length > 0) {
+ dummyDiv = makeDummyDiv(labels, "");
+ if (w == null)
+ w = dummyDiv.children().width();
+ if (h == null)
+ h = dummyDiv.find("div.tickLabel").height();
+ dummyDiv.remove();
+ }
+ }
+
+ if (w == null)
+ w = 0;
+ if (h == null)
+ h = 0;
+
+ axis.labelWidth = w;
+ axis.labelHeight = h;
+ }
+
+ function allocateAxisBoxFirstPhase(axis) {
+ // find the bounding box of the axis by looking at label
+ // widths/heights and ticks, make room by diminishing the
+ // plotOffset
+
+ var lw = axis.labelWidth,
+ lh = axis.labelHeight,
+ pos = axis.options.position,
+ tickLength = axis.options.tickLength,
+ axismargin = options.grid.axisMargin,
+ padding = options.grid.labelMargin,
+ all = axis.direction == "x" ? xaxes : yaxes,
+ index;
+
+ // determine axis margin
+ var samePosition = $.grep(all, function (a) {
+ return a && a.options.position == pos && a.reserveSpace;
+ });
+ if ($.inArray(axis, samePosition) == samePosition.length - 1)
+ axismargin = 0; // outermost
+
+ // determine tick length - if we're innermost, we can use "full"
+ if (tickLength == null)
+ tickLength = "full";
+
+ var sameDirection = $.grep(all, function (a) {
+ return a && a.reserveSpace;
+ });
+
+ var innermost = $.inArray(axis, sameDirection) == 0;
+ if (!innermost && tickLength == "full")
+ tickLength = 5;
+
+ if (!isNaN(+tickLength))
+ padding += +tickLength;
+
+ // compute box
+ if (axis.direction == "x") {
+ lh += padding;
+
+ if (pos == "bottom") {
+ plotOffset.bottom += lh + axismargin;
+ axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
+ }
+ else {
+ axis.box = { top: plotOffset.top + axismargin, height: lh };
+ plotOffset.top += lh + axismargin;
+ }
+ }
+ else {
+ lw += padding;
+
+ if (pos == "left") {
+ axis.box = { left: plotOffset.left + axismargin, width: lw };
+ plotOffset.left += lw + axismargin;
+ }
+ else {
+ plotOffset.right += lw + axismargin;
+ axis.box = { left: canvasWidth - plotOffset.right, width: lw };
+ }
+ }
+
+ // save for future reference
+ axis.position = pos;
+ axis.tickLength = tickLength;
+ axis.box.padding = padding;
+ axis.innermost = innermost;
+ }
+
+ function allocateAxisBoxSecondPhase(axis) {
+ // set remaining bounding box coordinates
+ if (axis.direction == "x") {
+ axis.box.left = plotOffset.left;
+ axis.box.width = plotWidth;
+ }
+ else {
+ axis.box.top = plotOffset.top;
+ axis.box.height = plotHeight;
+ }
+ }
+
+ function setupGrid() {
+ var i, axes = allAxes();
+
+ // first calculate the plot and axis box dimensions
+
+ $.each(axes, function (_, axis) {
+ axis.show = axis.options.show;
+ if (axis.show == null)
+ axis.show = axis.used; // by default an axis is visible if it's got data
+
+ axis.reserveSpace = axis.show || axis.options.reserveSpace;
+
+ setRange(axis);
+ });
+
+ allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
+
+ plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
+ if (options.grid.show) {
+ $.each(allocatedAxes, function (_, axis) {
+ // make the ticks
+ setupTickGeneration(axis);
+ setTicks(axis);
+ snapRangeToTicks(axis, axis.ticks);
+
+ // find labelWidth/Height for axis
+ measureTickLabels(axis);
+ });
+
+ // with all dimensions in house, we can compute the
+ // axis boxes, start from the outside (reverse order)
+ for (i = allocatedAxes.length - 1; i >= 0; --i)
+ allocateAxisBoxFirstPhase(allocatedAxes[i]);
+
+ // make sure we've got enough space for things that
+ // might stick out
+ var minMargin = options.grid.minBorderMargin;
+ if (minMargin == null) {
+ minMargin = 0;
+ for (i = 0; i < series.length; ++i)
+ minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
+ }
+
+ for (var a in plotOffset) {
+ plotOffset[a] += options.grid.borderWidth;
+ plotOffset[a] = Math.max(minMargin, plotOffset[a]);
+ }
+ }
+
+ plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
+ plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
+
+ // now we got the proper plotWidth/Height, we can compute the scaling
+ $.each(axes, function (_, axis) {
+ setTransformationHelpers(axis);
+ });
+
+ if (options.grid.show) {
+ $.each(allocatedAxes, function (_, axis) {
+ allocateAxisBoxSecondPhase(axis);
+ });
+
+ insertAxisLabels();
+ }
+
+ insertLegend();
+ }
+
+ function setRange(axis) {
+ var opts = axis.options,
+ min = +(opts.min != null ? opts.min : axis.datamin),
+ max = +(opts.max != null ? opts.max : axis.datamax),
+ delta = max - min;
+
+ if (delta == 0.0) {
+ // degenerate case
+ var widen = max == 0 ? 1 : 0.01;
+
+ if (opts.min == null)
+ min -= widen;
+ // always widen max if we couldn't widen min to ensure we
+ // don't fall into min == max which doesn't work
+ if (opts.max == null || opts.min != null)
+ max += widen;
+ }
+ else {
+ // consider autoscaling
+ var margin = opts.autoscaleMargin;
+ if (margin != null) {
+ if (opts.min == null) {
+ min -= delta * margin;
+ // make sure we don't go below zero if all values
+ // are positive
+ if (min < 0 && axis.datamin != null && axis.datamin >= 0)
+ min = 0;
+ }
+ if (opts.max == null) {
+ max += delta * margin;
+ if (max > 0 && axis.datamax != null && axis.datamax <= 0)
+ max = 0;
+ }
+ }
+ }
+ axis.min = min;
+ axis.max = max;
+ }
+
+ function setupTickGeneration(axis) {
+ var opts = axis.options;
+
+ // estimate number of ticks
+ var noTicks;
+ if (typeof opts.ticks == "number" && opts.ticks > 0)
+ noTicks = opts.ticks;
+ else
+ // heuristic based on the model a*sqrt(x) fitted to
+ // some data points that seemed reasonable
+ noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
+
+ var delta = (axis.max - axis.min) / noTicks,
+ size, generator, unit, formatter, i, magn, norm;
+
+ if (opts.mode == "time") {
+ // pretty handling of time
+
+ // map of app. size of time units in milliseconds
+ var timeUnitSize = {
+ "second": 1000,
+ "minute": 60 * 1000,
+ "hour": 60 * 60 * 1000,
+ "day": 24 * 60 * 60 * 1000,
+ "month": 30 * 24 * 60 * 60 * 1000,
+ "year": 365.2425 * 24 * 60 * 60 * 1000
+ };
+
+
+ // the allowed tick sizes, after 1 year we use
+ // an integer algorithm
+ var spec = [
+ [1, "second"], [2, "second"], [5, "second"], [10, "second"],
+ [30, "second"],
+ [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
+ [30, "minute"],
+ [1, "hour"], [2, "hour"], [4, "hour"],
+ [8, "hour"], [12, "hour"],
+ [1, "day"], [2, "day"], [3, "day"],
+ [0.25, "month"], [0.5, "month"], [1, "month"],
+ [2, "month"], [3, "month"], [6, "month"],
+ [1, "year"]
+ ];
+
+ var minSize = 0;
+ if (opts.minTickSize != null) {
+ if (typeof opts.tickSize == "number")
+ minSize = opts.tickSize;
+ else
+ minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
+ }
+
+ for (var i = 0; i < spec.length - 1; ++i)
+ if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+ + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
+ && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
+ break;
+ size = spec[i][0];
+ unit = spec[i][1];
+
+ // special-case the possibility of several years
+ if (unit == "year") {
+ magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
+ norm = (delta / timeUnitSize.year) / magn;
+ if (norm < 1.5)
+ size = 1;
+ else if (norm < 3)
+ size = 2;
+ else if (norm < 7.5)
+ size = 5;
+ else
+ size = 10;
+
+ size *= magn;
+ }
+
+ axis.tickSize = opts.tickSize || [size, unit];
+
+ generator = function(axis) {
+ var ticks = [],
+ tickSize = axis.tickSize[0], unit = axis.tickSize[1],
+ d = new Date(axis.min);
+
+ var step = tickSize * timeUnitSize[unit];
+
+ if (unit == "second")
+ d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
+ if (unit == "minute")
+ d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
+ if (unit == "hour")
+ d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
+ if (unit == "month")
+ d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
+ if (unit == "year")
+ d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
+
+ // reset smaller components
+ d.setUTCMilliseconds(0);
+ if (step >= timeUnitSize.minute)
+ d.setUTCSeconds(0);
+ if (step >= timeUnitSize.hour)
+ d.setUTCMinutes(0);
+ if (step >= timeUnitSize.day)
+ d.setUTCHours(0);
+ if (step >= timeUnitSize.day * 4)
+ d.setUTCDate(1);
+ if (step >= timeUnitSize.year)
+ d.setUTCMonth(0);
+
+
+ var carry = 0, v = Number.NaN, prev;
+ do {
+ prev = v;
+ v = d.getTime();
+ ticks.push(v);
+ if (unit == "month") {
+ if (tickSize < 1) {
+ // a bit complicated - we'll divide the month
+ // up but we need to take care of fractions
+ // so we don't end up in the middle of a day
+ d.setUTCDate(1);
+ var start = d.getTime();
+ d.setUTCMonth(d.getUTCMonth() + 1);
+ var end = d.getTime();
+ d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
+ carry = d.getUTCHours();
+ d.setUTCHours(0);
+ }
+ else
+ d.setUTCMonth(d.getUTCMonth() + tickSize);
+ }
+ else if (unit == "year") {
+ d.setUTCFullYear(d.getUTCFullYear() + tickSize);
+ }
+ else
+ d.setTime(v + step);
+ } while (v < axis.max && v != prev);
+
+ return ticks;
+ };
+
+ formatter = function (v, axis) {
+ var d = new Date(v);
+
+ // first check global format
+ if (opts.timeformat != null)
+ return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
+
+ var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
+ var span = axis.max - axis.min;
+ var suffix = (opts.twelveHourClock) ? " %p" : "";
+
+ if (t < timeUnitSize.minute)
+ fmt = "%h:%M:%S" + suffix;
+ else if (t < timeUnitSize.day) {
+ if (span < 2 * timeUnitSize.day)
+ fmt = "%h:%M" + suffix;
+ else
+ fmt = "%b %d %h:%M" + suffix;
+ }
+ else if (t < timeUnitSize.month)
+ fmt = "%b %d";
+ else if (t < timeUnitSize.year) {
+ if (span < timeUnitSize.year)
+ fmt = "%b";
+ else
+ fmt = "%b %y";
+ }
+ else
+ fmt = "%y";
+
+ return $.plot.formatDate(d, fmt, opts.monthNames);
+ };
+ }
+ else {
+ // pretty rounding of base-10 numbers
+ var maxDec = opts.tickDecimals;
+ var dec = -Math.floor(Math.log(delta) / Math.LN10);
+ if (maxDec != null && dec > maxDec)
+ dec = maxDec;
+
+ magn = Math.pow(10, -dec);
+ norm = delta / magn; // norm is between 1.0 and 10.0
+
+ if (norm < 1.5)
+ size = 1;
+ else if (norm < 3) {
+ size = 2;
+ // special case for 2.5, requires an extra decimal
+ if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
+ size = 2.5;
+ ++dec;
+ }
+ }
+ else if (norm < 7.5)
+ size = 5;
+ else
+ size = 10;
+
+ size *= magn;
+
+ if (opts.minTickSize != null && size < opts.minTickSize)
+ size = opts.minTickSize;
+
+ axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
+ axis.tickSize = opts.tickSize || size;
+
+ generator = function (axis) {
+ var ticks = [];
+
+ // spew out all possible ticks
+ var start = floorInBase(axis.min, axis.tickSize),
+ i = 0, v = Number.NaN, prev;
+ do {
+ prev = v;
+ v = start + i * axis.tickSize;
+ ticks.push(v);
+ ++i;
+ } while (v < axis.max && v != prev);
+ return ticks;
+ };
+
+ formatter = function (v, axis) {
+ return v.toFixed(axis.tickDecimals);
+ };
+ }
+
+ if (opts.alignTicksWithAxis != null) {
+ var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
+ if (otherAxis && otherAxis.used && otherAxis != axis) {
+ // consider snapping min/max to outermost nice ticks
+ var niceTicks = generator(axis);
+ if (niceTicks.length > 0) {
+ if (opts.min == null)
+ axis.min = Math.min(axis.min, niceTicks[0]);
+ if (opts.max == null && niceTicks.length > 1)
+ axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
+ }
+
+ generator = function (axis) {
+ // copy ticks, scaled to this axis
+ var ticks = [], v, i;
+ for (i = 0; i < otherAxis.ticks.length; ++i) {
+ v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
+ v = axis.min + v * (axis.max - axis.min);
+ ticks.push(v);
+ }
+ return ticks;
+ };
+
+ // we might need an extra decimal since forced
+ // ticks don't necessarily fit naturally
+ if (axis.mode != "time" && opts.tickDecimals == null) {
+ var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
+ ts = generator(axis);
+
+ // only proceed if the tick interval rounded
+ // with an extra decimal doesn't give us a
+ // zero at end
+ if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
+ axis.tickDecimals = extraDec;
+ }
+ }
+ }
+
+ axis.tickGenerator = generator;
+ if ($.isFunction(opts.tickFormatter))
+ axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
+ else
+ axis.tickFormatter = formatter;
+ }
+
+ function setTicks(axis) {
+ var oticks = axis.options.ticks, ticks = [];
+ if (oticks == null || (typeof oticks == "number" && oticks > 0))
+ ticks = axis.tickGenerator(axis);
+ else if (oticks) {
+ if ($.isFunction(oticks))
+ // generate the ticks
+ ticks = oticks({ min: axis.min, max: axis.max });
+ else
+ ticks = oticks;
+ }
+
+ // clean up/labelify the supplied ticks, copy them over
+ var i, v;
+ axis.ticks = [];
+ for (i = 0; i < ticks.length; ++i) {
+ var label = null;
+ var t = ticks[i];
+ if (typeof t == "object") {
+ v = +t[0];
+ if (t.length > 1)
+ label = t[1];
+ }
+ else
+ v = +t;
+ if (label == null)
+ label = axis.tickFormatter(v, axis);
+ if (!isNaN(v))
+ axis.ticks.push({ v: v, label: label });
+ }
+ }
+
+ function snapRangeToTicks(axis, ticks) {
+ if (axis.options.autoscaleMargin && ticks.length > 0) {
+ // snap to ticks
+ if (axis.options.min == null)
+ axis.min = Math.min(axis.min, ticks[0].v);
+ if (axis.options.max == null && ticks.length > 1)
+ axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
+ }
+ }
+
+ function draw() {
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+ var grid = options.grid;
+
+ // draw background, if any
+ if (grid.show && grid.backgroundColor)
+ drawBackground();
+
+ if (grid.show && !grid.aboveData)
+ drawGrid();
+
+ for (var i = 0; i < series.length; ++i) {
+ executeHooks(hooks.drawSeries, [ctx, series[i]]);
+ drawSeries(series[i]);
+ }
+
+ executeHooks(hooks.draw, [ctx]);
+
+ if (grid.show && grid.aboveData)
+ drawGrid();
+ }
+
+ function extractRange(ranges, coord) {
+ var axis, from, to, key, axes = allAxes();
+
+ for (i = 0; i < axes.length; ++i) {
+ axis = axes[i];
+ if (axis.direction == coord) {
+ key = coord + axis.n + "axis";
+ if (!ranges[key] && axis.n == 1)
+ key = coord + "axis"; // support x1axis as xaxis
+ if (ranges[key]) {
+ from = ranges[key].from;
+ to = ranges[key].to;
+ break;
+ }
+ }
+ }
+
+ // backwards-compat stuff - to be removed in future
+ if (!ranges[key]) {
+ axis = coord == "x" ? xaxes[0] : yaxes[0];
+ from = ranges[coord + "1"];
+ to = ranges[coord + "2"];
+ }
+
+ // auto-reverse as an added bonus
+ if (from != null && to != null && from > to) {
+ var tmp = from;
+ from = to;
+ to = tmp;
+ }
+
+ return { from: from, to: to, axis: axis };
+ }
+
+ function drawBackground() {
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
+ ctx.fillRect(0, 0, plotWidth, plotHeight);
+ ctx.restore();
+ }
+
+ function drawGrid() {
+ var i;
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ // draw markings
+ var markings = options.grid.markings;
+ if (markings) {
+ if ($.isFunction(markings)) {
+ var axes = plot.getAxes();
+ // xmin etc. is backwards compatibility, to be
+ // removed in the future
+ axes.xmin = axes.xaxis.min;
+ axes.xmax = axes.xaxis.max;
+ axes.ymin = axes.yaxis.min;
+ axes.ymax = axes.yaxis.max;
+
+ markings = markings(axes);
+ }
+
+ for (i = 0; i < markings.length; ++i) {
+ var m = markings[i],
+ xrange = extractRange(m, "x"),
+ yrange = extractRange(m, "y");
+
+ // fill in missing
+ if (xrange.from == null)
+ xrange.from = xrange.axis.min;
+ if (xrange.to == null)
+ xrange.to = xrange.axis.max;
+ if (yrange.from == null)
+ yrange.from = yrange.axis.min;
+ if (yrange.to == null)
+ yrange.to = yrange.axis.max;
+
+ // clip
+ if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
+ yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
+ continue;
+
+ xrange.from = Math.max(xrange.from, xrange.axis.min);
+ xrange.to = Math.min(xrange.to, xrange.axis.max);
+ yrange.from = Math.max(yrange.from, yrange.axis.min);
+ yrange.to = Math.min(yrange.to, yrange.axis.max);
+
+ if (xrange.from == xrange.to && yrange.from == yrange.to)
+ continue;
+
+ // then draw
+ xrange.from = xrange.axis.p2c(xrange.from);
+ xrange.to = xrange.axis.p2c(xrange.to);
+ yrange.from = yrange.axis.p2c(yrange.from);
+ yrange.to = yrange.axis.p2c(yrange.to);
+
+ if (xrange.from == xrange.to || yrange.from == yrange.to) {
+ // draw line
+ ctx.beginPath();
+ ctx.strokeStyle = m.color || options.grid.markingsColor;
+ ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
+ ctx.moveTo(xrange.from, yrange.from);
+ ctx.lineTo(xrange.to, yrange.to);
+ ctx.stroke();
+ }
+ else {
+ // fill area
+ ctx.fillStyle = m.color || options.grid.markingsColor;
+ ctx.fillRect(xrange.from, yrange.to,
+ xrange.to - xrange.from,
+ yrange.from - yrange.to);
+ }
+ }
+ }
+
+ // draw the ticks
+ var axes = allAxes(), bw = options.grid.borderWidth;
+
+ for (var j = 0; j < axes.length; ++j) {
+ var axis = axes[j], box = axis.box,
+ t = axis.tickLength, x, y, xoff, yoff;
+ if (!axis.show || axis.ticks.length == 0)
+ continue
+
+ ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
+ ctx.lineWidth = 1;
+
+ // find the edges
+ if (axis.direction == "x") {
+ x = 0;
+ if (t == "full")
+ y = (axis.position == "top" ? 0 : plotHeight);
+ else
+ y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
+ }
+ else {
+ y = 0;
+ if (t == "full")
+ x = (axis.position == "left" ? 0 : plotWidth);
+ else
+ x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
+ }
+
+ // draw tick bar
+ if (!axis.innermost) {
+ ctx.beginPath();
+ xoff = yoff = 0;
+ if (axis.direction == "x")
+ xoff = plotWidth;
+ else
+ yoff = plotHeight;
+
+ if (ctx.lineWidth == 1) {
+ x = Math.floor(x) + 0.5;
+ y = Math.floor(y) + 0.5;
+ }
+
+ ctx.moveTo(x, y);
+ ctx.lineTo(x + xoff, y + yoff);
+ ctx.stroke();
+ }
+
+ // draw ticks
+ ctx.beginPath();
+ for (i = 0; i < axis.ticks.length; ++i) {
+ var v = axis.ticks[i].v;
+
+ xoff = yoff = 0;
+
+ if (v < axis.min || v > axis.max
+ // skip those lying on the axes if we got a border
+ || (t == "full" && bw > 0
+ && (v == axis.min || v == axis.max)))
+ continue;
+
+ if (axis.direction == "x") {
+ x = axis.p2c(v);
+ yoff = t == "full" ? -plotHeight : t;
+
+ if (axis.position == "top")
+ yoff = -yoff;
+ }
+ else {
+ y = axis.p2c(v);
+ xoff = t == "full" ? -plotWidth : t;
+
+ if (axis.position == "left")
+ xoff = -xoff;
+ }
+
+ if (ctx.lineWidth == 1) {
+ if (axis.direction == "x")
+ x = Math.floor(x) + 0.5;
+ else
+ y = Math.floor(y) + 0.5;
+ }
+
+ ctx.moveTo(x, y);
+ ctx.lineTo(x + xoff, y + yoff);
+ }
+
+ ctx.stroke();
+ }
+
+
+ // draw border
+ if (bw) {
+ ctx.lineWidth = bw;
+ ctx.strokeStyle = options.grid.borderColor;
+ ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
+ }
+
+ ctx.restore();
+ }
+
+ function insertAxisLabels() {
+ placeholder.find(".tickLabels").remove();
+
+ var html = [''];
+
+ var axes = allAxes();
+ for (var j = 0; j < axes.length; ++j) {
+ var axis = axes[j], box = axis.box;
+ if (!axis.show)
+ continue;
+ //debug: html.push('
')
+ html.push('
');
+ for (var i = 0; i < axis.ticks.length; ++i) {
+ var tick = axis.ticks[i];
+ if (!tick.label || tick.v < axis.min || tick.v > axis.max)
+ continue;
+
+ var pos = {}, align;
+
+ if (axis.direction == "x") {
+ align = "center";
+ pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
+ if (axis.position == "bottom")
+ pos.top = box.top + box.padding;
+ else
+ pos.bottom = canvasHeight - (box.top + box.height - box.padding);
+ }
+ else {
+ pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
+ if (axis.position == "left") {
+ pos.right = canvasWidth - (box.left + box.width - box.padding)
+ align = "right";
+ }
+ else {
+ pos.left = box.left + box.padding;
+ align = "left";
+ }
+ }
+
+ pos.width = axis.labelWidth;
+
+ var style = ["position:absolute", "text-align:" + align ];
+ for (var a in pos)
+ style.push(a + ":" + pos[a] + "px")
+
+ html.push('
' + tick.label + '
');
+ }
+ html.push('
');
+ }
+
+ html.push('
');
+
+ placeholder.append(html.join(""));
+ }
+
+ function drawSeries(series) {
+ if (series.lines.show)
+ drawSeriesLines(series);
+ if (series.bars.show)
+ drawSeriesBars(series);
+ if (series.points.show)
+ drawSeriesPoints(series);
+ }
+
+ function drawSeriesLines(series) {
+ function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize,
+ prevx = null, prevy = null;
+
+ ctx.beginPath();
+ for (var i = ps; i < points.length; i += ps) {
+ var x1 = points[i - ps], y1 = points[i - ps + 1],
+ x2 = points[i], y2 = points[i + 1];
+
+ if (x1 == null || x2 == null)
+ continue;
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < axisy.min) {
+ if (y2 < axisy.min)
+ continue; // line segment is outside
+ // compute new intersection point
+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.min;
+ }
+ else if (y2 <= y1 && y2 < axisy.min) {
+ if (y1 < axisy.min)
+ continue;
+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.min;
+ }
+
+ // clip with ymax
+ if (y1 >= y2 && y1 > axisy.max) {
+ if (y2 > axisy.max)
+ continue;
+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.max;
+ }
+ else if (y2 >= y1 && y2 > axisy.max) {
+ if (y1 > axisy.max)
+ continue;
+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.max;
+ }
+
+ // clip with xmin
+ if (x1 <= x2 && x1 < axisx.min) {
+ if (x2 < axisx.min)
+ continue;
+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = axisx.min;
+ }
+ else if (x2 <= x1 && x2 < axisx.min) {
+ if (x1 < axisx.min)
+ continue;
+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = axisx.min;
+ }
+
+ // clip with xmax
+ if (x1 >= x2 && x1 > axisx.max) {
+ if (x2 > axisx.max)
+ continue;
+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = axisx.max;
+ }
+ else if (x2 >= x1 && x2 > axisx.max) {
+ if (x1 > axisx.max)
+ continue;
+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = axisx.max;
+ }
+
+ if (x1 != prevx || y1 != prevy)
+ ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+
+ prevx = x2;
+ prevy = y2;
+ ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+ }
+ ctx.stroke();
+ }
+
+ function plotLineArea(datapoints, axisx, axisy) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize,
+ bottom = Math.min(Math.max(0, axisy.min), axisy.max),
+ i = 0, top, areaOpen = false,
+ ypos = 1, segmentStart = 0, segmentEnd = 0;
+
+ // we process each segment in two turns, first forward
+ // direction to sketch out top, then once we hit the
+ // end we go backwards to sketch the bottom
+ while (true) {
+ if (ps > 0 && i > points.length + ps)
+ break;
+
+ i += ps; // ps is negative if going backwards
+
+ var x1 = points[i - ps],
+ y1 = points[i - ps + ypos],
+ x2 = points[i], y2 = points[i + ypos];
+
+ if (areaOpen) {
+ if (ps > 0 && x1 != null && x2 == null) {
+ // at turning point
+ segmentEnd = i;
+ ps = -ps;
+ ypos = 2;
+ continue;
+ }
+
+ if (ps < 0 && i == segmentStart + ps) {
+ // done with the reverse sweep
+ ctx.fill();
+ areaOpen = false;
+ ps = -ps;
+ ypos = 1;
+ i = segmentStart = segmentEnd + ps;
+ continue;
+ }
+ }
+
+ if (x1 == null || x2 == null)
+ continue;
+
+ // clip x values
+
+ // clip with xmin
+ if (x1 <= x2 && x1 < axisx.min) {
+ if (x2 < axisx.min)
+ continue;
+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = axisx.min;
+ }
+ else if (x2 <= x1 && x2 < axisx.min) {
+ if (x1 < axisx.min)
+ continue;
+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = axisx.min;
+ }
+
+ // clip with xmax
+ if (x1 >= x2 && x1 > axisx.max) {
+ if (x2 > axisx.max)
+ continue;
+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x1 = axisx.max;
+ }
+ else if (x2 >= x1 && x2 > axisx.max) {
+ if (x1 > axisx.max)
+ continue;
+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
+ x2 = axisx.max;
+ }
+
+ if (!areaOpen) {
+ // open area
+ ctx.beginPath();
+ ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+ areaOpen = true;
+ }
+
+ // now first check the case where both is outside
+ if (y1 >= axisy.max && y2 >= axisy.max) {
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+ continue;
+ }
+ else if (y1 <= axisy.min && y2 <= axisy.min) {
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+ continue;
+ }
+
+ // else it's a bit more complicated, there might
+ // be a flat maxed out rectangle first, then a
+ // triangular cutout or reverse; to find these
+ // keep track of the current x values
+ var x1old = x1, x2old = x2;
+
+ // clip the y values, without shortcutting, we
+ // go through all cases in turn
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.min;
+ }
+ else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.min;
+ }
+
+ // clip with ymax
+ if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.max;
+ }
+ else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.max;
+ }
+
+ // if the x value was changed we got a rectangle
+ // to fill
+ if (x1 != x1old) {
+ ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
+ // it goes to (x1, y1), but we fill that below
+ }
+
+ // fill triangular section, this sometimes result
+ // in redundant points if (x1, y1) hasn't changed
+ // from previous line to, but we just ignore that
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+ // fill the other rectangle if it's there
+ if (x2 != x2old) {
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+ ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
+ }
+ }
+ }
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+ ctx.lineJoin = "round";
+
+ var lw = series.lines.lineWidth,
+ sw = series.shadowSize;
+ // FIXME: consider another form of shadow when filling is turned on
+ if (lw > 0 && sw > 0) {
+ // draw shadow as a thick and thin line with transparency
+ ctx.lineWidth = sw;
+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
+ // position shadow at angle from the mid of line
+ var angle = Math.PI/18;
+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
+ ctx.lineWidth = sw/2;
+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
+ }
+
+ ctx.lineWidth = lw;
+ ctx.strokeStyle = series.color;
+ var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
+ if (fillStyle) {
+ ctx.fillStyle = fillStyle;
+ plotLineArea(series.datapoints, series.xaxis, series.yaxis);
+ }
+
+ if (lw > 0)
+ plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
+ ctx.restore();
+ }
+
+ function drawSeriesPoints(series) {
+ function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
+ var points = datapoints.points, ps = datapoints.pointsize;
+
+ for (var i = 0; i < points.length; i += ps) {
+ var x = points[i], y = points[i + 1];
+ if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+ continue;
+
+ ctx.beginPath();
+ x = axisx.p2c(x);
+ y = axisy.p2c(y) + offset;
+ if (symbol == "circle")
+ ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
+ else
+ symbol(ctx, x, y, radius, shadow);
+ ctx.closePath();
+
+ if (fillStyle) {
+ ctx.fillStyle = fillStyle;
+ ctx.fill();
+ }
+ ctx.stroke();
+ }
+ }
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ var lw = series.points.lineWidth,
+ sw = series.shadowSize,
+ radius = series.points.radius,
+ symbol = series.points.symbol;
+ if (lw > 0 && sw > 0) {
+ // draw shadow in two steps
+ var w = sw / 2;
+ ctx.lineWidth = w;
+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
+ plotPoints(series.datapoints, radius, null, w + w/2, true,
+ series.xaxis, series.yaxis, symbol);
+
+ ctx.strokeStyle = "rgba(0,0,0,0.2)";
+ plotPoints(series.datapoints, radius, null, w/2, true,
+ series.xaxis, series.yaxis, symbol);
+ }
+
+ ctx.lineWidth = lw;
+ ctx.strokeStyle = series.color;
+ plotPoints(series.datapoints, radius,
+ getFillStyle(series.points, series.color), 0, false,
+ series.xaxis, series.yaxis, symbol);
+ ctx.restore();
+ }
+
+ function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
+ var left, right, bottom, top,
+ drawLeft, drawRight, drawTop, drawBottom,
+ tmp;
+
+ // in horizontal mode, we start the bar from the left
+ // instead of from the bottom so it appears to be
+ // horizontal rather than vertical
+ if (horizontal) {
+ drawBottom = drawRight = drawTop = true;
+ drawLeft = false;
+ left = b;
+ right = x;
+ top = y + barLeft;
+ bottom = y + barRight;
+
+ // account for negative bars
+ if (right < left) {
+ tmp = right;
+ right = left;
+ left = tmp;
+ drawLeft = true;
+ drawRight = false;
+ }
+ }
+ else {
+ drawLeft = drawRight = drawTop = true;
+ drawBottom = false;
+ left = x + barLeft;
+ right = x + barRight;
+ bottom = b;
+ top = y;
+
+ // account for negative bars
+ if (top < bottom) {
+ tmp = top;
+ top = bottom;
+ bottom = tmp;
+ drawBottom = true;
+ drawTop = false;
+ }
+ }
+
+ // clip
+ if (right < axisx.min || left > axisx.max ||
+ top < axisy.min || bottom > axisy.max)
+ return;
+
+ if (left < axisx.min) {
+ left = axisx.min;
+ drawLeft = false;
+ }
+
+ if (right > axisx.max) {
+ right = axisx.max;
+ drawRight = false;
+ }
+
+ if (bottom < axisy.min) {
+ bottom = axisy.min;
+ drawBottom = false;
+ }
+
+ if (top > axisy.max) {
+ top = axisy.max;
+ drawTop = false;
+ }
+
+ left = axisx.p2c(left);
+ bottom = axisy.p2c(bottom);
+ right = axisx.p2c(right);
+ top = axisy.p2c(top);
+
+ // fill the bar
+ if (fillStyleCallback) {
+ c.beginPath();
+ c.moveTo(left, bottom);
+ c.lineTo(left, top);
+ c.lineTo(right, top);
+ c.lineTo(right, bottom);
+ c.fillStyle = fillStyleCallback(bottom, top);
+ c.fill();
+ }
+
+ // draw outline
+ if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
+ c.beginPath();
+
+ // FIXME: inline moveTo is buggy with excanvas
+ c.moveTo(left, bottom + offset);
+ if (drawLeft)
+ c.lineTo(left, top + offset);
+ else
+ c.moveTo(left, top + offset);
+ if (drawTop)
+ c.lineTo(right, top + offset);
+ else
+ c.moveTo(right, top + offset);
+ if (drawRight)
+ c.lineTo(right, bottom + offset);
+ else
+ c.moveTo(right, bottom + offset);
+ if (drawBottom)
+ c.lineTo(left, bottom + offset);
+ else
+ c.moveTo(left, bottom + offset);
+ c.stroke();
+ }
+ }
+
+ function drawSeriesBars(series) {
+ function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
+ var points = datapoints.points, ps = datapoints.pointsize;
+
+ for (var i = 0; i < points.length; i += ps) {
+ if (points[i] == null)
+ continue;
+ drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
+ }
+ }
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ // FIXME: figure out a way to add shadows (for instance along the right edge)
+ ctx.lineWidth = series.bars.lineWidth;
+ ctx.strokeStyle = series.color;
+ var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+ var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
+ plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
+ ctx.restore();
+ }
+
+ function getFillStyle(filloptions, seriesColor, bottom, top) {
+ var fill = filloptions.fill;
+ if (!fill)
+ return null;
+
+ if (filloptions.fillColor)
+ return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+
+ var c = $.color.parse(seriesColor);
+ c.a = typeof fill == "number" ? fill : 0.4;
+ c.normalize();
+ return c.toString();
+ }
+
+ function insertLegend() {
+ placeholder.find(".legend").remove();
+
+ if (!options.legend.show)
+ return;
+
+ var fragments = [], rowStarted = false,
+ lf = options.legend.labelFormatter, s, label;
+ for (var i = 0; i < series.length; ++i) {
+ s = series[i];
+ label = s.label;
+ if (!label)
+ continue;
+
+ if (i % options.legend.noColumns == 0) {
+ if (rowStarted)
+ fragments.push('');
+ fragments.push('');
+ rowStarted = true;
+ }
+
+ if (lf)
+ label = lf(label, s);
+
+ fragments.push(
+ ' ' +
+ '' + label + ' ');
+ }
+ if (rowStarted)
+ fragments.push(' ');
+
+ if (fragments.length == 0)
+ return;
+
+ var table = '' + fragments.join("") + '
';
+ if (options.legend.container != null)
+ $(options.legend.container).html(table);
+ else {
+ var pos = "",
+ p = options.legend.position,
+ m = options.legend.margin;
+ if (m[0] == null)
+ m = [m, m];
+ if (p.charAt(0) == "n")
+ pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
+ else if (p.charAt(0) == "s")
+ pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
+ if (p.charAt(1) == "e")
+ pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
+ else if (p.charAt(1) == "w")
+ pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
+ var legend = $('' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder);
+ if (options.legend.backgroundOpacity != 0.0) {
+ // put in the transparent background
+ // separately to avoid blended labels and
+ // label boxes
+ var c = options.legend.backgroundColor;
+ if (c == null) {
+ c = options.grid.backgroundColor;
+ if (c && typeof c == "string")
+ c = $.color.parse(c);
+ else
+ c = $.color.extract(legend, 'background-color');
+ c.a = 1;
+ c = c.toString();
+ }
+ var div = legend.children();
+ $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
+ }
+ }
+ }
+
+
+ // interactive features
+
+ var highlights = [],
+ redrawTimeout = null;
+
+ // returns the data item the mouse is over, or null if none is found
+ function findNearbyItem(mouseX, mouseY, seriesFilter) {
+ var maxDistance = options.grid.mouseActiveRadius,
+ smallestDistance = maxDistance * maxDistance + 1,
+ item = null, foundPoint = false, i, j;
+
+ for (i = series.length - 1; i >= 0; --i) {
+ if (!seriesFilter(series[i]))
+ continue;
+
+ var s = series[i],
+ axisx = s.xaxis,
+ axisy = s.yaxis,
+ points = s.datapoints.points,
+ ps = s.datapoints.pointsize,
+ mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
+ my = axisy.c2p(mouseY),
+ maxx = maxDistance / axisx.scale,
+ maxy = maxDistance / axisy.scale;
+
+ // with inverse transforms, we can't use the maxx/maxy
+ // optimization, sadly
+ if (axisx.options.inverseTransform)
+ maxx = Number.MAX_VALUE;
+ if (axisy.options.inverseTransform)
+ maxy = Number.MAX_VALUE;
+
+ if (s.lines.show || s.points.show) {
+ for (j = 0; j < points.length; j += ps) {
+ var x = points[j], y = points[j + 1];
+ if (x == null)
+ continue;
+
+ // For points and lines, the cursor must be within a
+ // certain distance to the data point
+ if (x - mx > maxx || x - mx < -maxx ||
+ y - my > maxy || y - my < -maxy)
+ continue;
+
+ // We have to calculate distances in pixels, not in
+ // data units, because the scales of the axes may be different
+ var dx = Math.abs(axisx.p2c(x) - mouseX),
+ dy = Math.abs(axisy.p2c(y) - mouseY),
+ dist = dx * dx + dy * dy; // we save the sqrt
+
+ // use <= to ensure last point takes precedence
+ // (last generally means on top of)
+ if (dist < smallestDistance) {
+ smallestDistance = dist;
+ item = [i, j / ps];
+ }
+ }
+ }
+
+ if (s.bars.show && !item) { // no other point can be nearby
+ var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
+ barRight = barLeft + s.bars.barWidth;
+
+ for (j = 0; j < points.length; j += ps) {
+ var x = points[j], y = points[j + 1], b = points[j + 2];
+ if (x == null)
+ continue;
+
+ // for a bar graph, the cursor must be inside the bar
+ if (series[i].bars.horizontal ?
+ (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
+ my >= y + barLeft && my <= y + barRight) :
+ (mx >= x + barLeft && mx <= x + barRight &&
+ my >= Math.min(b, y) && my <= Math.max(b, y)))
+ item = [i, j / ps];
+ }
+ }
+ }
+
+ if (item) {
+ i = item[0];
+ j = item[1];
+ ps = series[i].datapoints.pointsize;
+
+ return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
+ dataIndex: j,
+ series: series[i],
+ seriesIndex: i };
+ }
+
+ return null;
+ }
+
+ function onMouseMove(e) {
+ if (options.grid.hoverable)
+ triggerClickHoverEvent("plothover", e,
+ function (s) { return s["hoverable"] != false; });
+ }
+
+ function onMouseLeave(e) {
+ if (options.grid.hoverable)
+ triggerClickHoverEvent("plothover", e,
+ function (s) { return false; });
+ }
+
+ function onClick(e) {
+ triggerClickHoverEvent("plotclick", e,
+ function (s) { return s["clickable"] != false; });
+ }
+
+ // trigger click or hover event (they send the same parameters
+ // so we share their code)
+ function triggerClickHoverEvent(eventname, event, seriesFilter) {
+ var offset = eventHolder.offset(),
+ canvasX = event.pageX - offset.left - plotOffset.left,
+ canvasY = event.pageY - offset.top - plotOffset.top,
+ pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
+
+ pos.pageX = event.pageX;
+ pos.pageY = event.pageY;
+
+ var item = findNearbyItem(canvasX, canvasY, seriesFilter);
+
+ if (item) {
+ // fill in mouse pos for any listeners out there
+ item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
+ item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
+ }
+
+ if (options.grid.autoHighlight) {
+ // clear auto-highlights
+ for (var i = 0; i < highlights.length; ++i) {
+ var h = highlights[i];
+ if (h.auto == eventname &&
+ !(item && h.series == item.series &&
+ h.point[0] == item.datapoint[0] &&
+ h.point[1] == item.datapoint[1]))
+ unhighlight(h.series, h.point);
+ }
+
+ if (item)
+ highlight(item.series, item.datapoint, eventname);
+ }
+
+ placeholder.trigger(eventname, [ pos, item ]);
+ }
+
+ function triggerRedrawOverlay() {
+ if (!redrawTimeout)
+ redrawTimeout = setTimeout(drawOverlay, 30);
+ }
+
+ function drawOverlay() {
+ redrawTimeout = null;
+
+ // draw highlights
+ octx.save();
+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
+ octx.translate(plotOffset.left, plotOffset.top);
+
+ var i, hi;
+ for (i = 0; i < highlights.length; ++i) {
+ hi = highlights[i];
+
+ if (hi.series.bars.show)
+ drawBarHighlight(hi.series, hi.point);
+ else
+ drawPointHighlight(hi.series, hi.point);
+ }
+ octx.restore();
+
+ executeHooks(hooks.drawOverlay, [octx]);
+ }
+
+ function highlight(s, point, auto) {
+ if (typeof s == "number")
+ s = series[s];
+
+ if (typeof point == "number") {
+ var ps = s.datapoints.pointsize;
+ point = s.datapoints.points.slice(ps * point, ps * (point + 1));
+ }
+
+ var i = indexOfHighlight(s, point);
+ if (i == -1) {
+ highlights.push({ series: s, point: point, auto: auto });
+
+ triggerRedrawOverlay();
+ }
+ else if (!auto)
+ highlights[i].auto = false;
+ }
+
+ function unhighlight(s, point) {
+ if (s == null && point == null) {
+ highlights = [];
+ triggerRedrawOverlay();
+ }
+
+ if (typeof s == "number")
+ s = series[s];
+
+ if (typeof point == "number")
+ point = s.data[point];
+
+ var i = indexOfHighlight(s, point);
+ if (i != -1) {
+ highlights.splice(i, 1);
+
+ triggerRedrawOverlay();
+ }
+ }
+
+ function indexOfHighlight(s, p) {
+ for (var i = 0; i < highlights.length; ++i) {
+ var h = highlights[i];
+ if (h.series == s && h.point[0] == p[0]
+ && h.point[1] == p[1])
+ return i;
+ }
+ return -1;
+ }
+
+ function drawPointHighlight(series, point) {
+ var x = point[0], y = point[1],
+ axisx = series.xaxis, axisy = series.yaxis;
+
+ if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
+ return;
+
+ var pointRadius = series.points.radius + series.points.lineWidth / 2;
+ octx.lineWidth = pointRadius;
+ octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+ var radius = 1.5 * pointRadius,
+ x = axisx.p2c(x),
+ y = axisy.p2c(y);
+
+ octx.beginPath();
+ if (series.points.symbol == "circle")
+ octx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ else
+ series.points.symbol(octx, x, y, radius, false);
+ octx.closePath();
+ octx.stroke();
+ }
+
+ function drawBarHighlight(series, point) {
+ octx.lineWidth = series.bars.lineWidth;
+ octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+ var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
+ var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
+ drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
+ 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
+ }
+
+ function getColorOrGradient(spec, bottom, top, defaultColor) {
+ if (typeof spec == "string")
+ return spec;
+ else {
+ // assume this is a gradient spec; IE currently only
+ // supports a simple vertical gradient properly, so that's
+ // what we support too
+ var gradient = ctx.createLinearGradient(0, top, 0, bottom);
+
+ for (var i = 0, l = spec.colors.length; i < l; ++i) {
+ var c = spec.colors[i];
+ if (typeof c != "string") {
+ var co = $.color.parse(defaultColor);
+ if (c.brightness != null)
+ co = co.scale('rgb', c.brightness)
+ if (c.opacity != null)
+ co.a *= c.opacity;
+ c = co.toString();
+ }
+ gradient.addColorStop(i / (l - 1), c);
+ }
+
+ return gradient;
+ }
+ }
+ }
+
+ $.plot = function(placeholder, data, options) {
+ //var t0 = new Date();
+ var plot = new Plot($(placeholder), data, options, $.plot.plugins);
+ //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
+ return plot;
+ };
+
+ $.plot.version = "0.7";
+
+ $.plot.plugins = [];
+
+ // returns a string with the date d formatted according to fmt
+ $.plot.formatDate = function(d, fmt, monthNames) {
+ var leftPad = function(n) {
+ n = "" + n;
+ return n.length == 1 ? "0" + n : n;
+ };
+
+ var r = [];
+ var escape = false, padNext = false;
+ var hours = d.getUTCHours();
+ var isAM = hours < 12;
+ if (monthNames == null)
+ monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ if (fmt.search(/%p|%P/) != -1) {
+ if (hours > 12) {
+ hours = hours - 12;
+ } else if (hours == 0) {
+ hours = 12;
+ }
+ }
+ for (var i = 0; i < fmt.length; ++i) {
+ var c = fmt.charAt(i);
+
+ if (escape) {
+ switch (c) {
+ case 'h': c = "" + hours; break;
+ case 'H': c = leftPad(hours); break;
+ case 'M': c = leftPad(d.getUTCMinutes()); break;
+ case 'S': c = leftPad(d.getUTCSeconds()); break;
+ case 'd': c = "" + d.getUTCDate(); break;
+ case 'm': c = "" + (d.getUTCMonth() + 1); break;
+ case 'y': c = "" + d.getUTCFullYear(); break;
+ case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
+ case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
+ case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
+ case '0': c = ""; padNext = true; break;
+ }
+ if (c && padNext) {
+ c = leftPad(c);
+ padNext = false;
+ }
+ r.push(c);
+ if (!padNext)
+ escape = false;
+ }
+ else {
+ if (c == "%")
+ escape = true;
+ else
+ r.push(c);
+ }
+ }
+ return r.join("");
+ };
+
+ // round to nearby lower multiple of base
+ function floorInBase(n, base) {
+ return base * Math.floor(n / base);
+ }
+
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.min.js
new file mode 100644
index 0000000000..4467fc5d8c
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.min.js
@@ -0,0 +1,6 @@
+/* Javascript plotting library for jQuery, v. 0.7.
+ *
+ * Released under the MIT license by IOLA, December 2007.
+ *
+ */
+(function(b){b.color={};b.color.make=function(d,e,g,f){var c={};c.r=d||0;c.g=e||0;c.b=g||0;c.a=f!=null?f:1;c.add=function(h,j){for(var k=0;k=1){return"rgb("+[c.r,c.g,c.b].join(",")+")"}else{return"rgba("+[c.r,c.g,c.b,c.a].join(",")+")"}};c.normalize=function(){function h(k,j,l){return jl?l:j)}c.r=h(0,parseInt(c.r),255);c.g=h(0,parseInt(c.g),255);c.b=h(0,parseInt(c.b),255);c.a=h(0,c.a,1);return c};c.clone=function(){return b.color.make(c.r,c.b,c.g,c.a)};return c.normalize()};b.color.extract=function(d,e){var c;do{c=d.css(e).toLowerCase();if(c!=""&&c!="transparent"){break}d=d.parent()}while(!b.nodeName(d.get(0),"body"));if(c=="rgba(0, 0, 0, 0)"){c="transparent"}return b.color.parse(c)};b.color.parse=function(c){var d,f=b.color.make;if(d=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10))}if(d=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10),parseFloat(d[4]))}if(d=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55)}if(d=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55,parseFloat(d[4]))}if(d=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c)){return f(parseInt(d[1],16),parseInt(d[2],16),parseInt(d[3],16))}if(d=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c)){return f(parseInt(d[1]+d[1],16),parseInt(d[2]+d[2],16),parseInt(d[3]+d[3],16))}var e=b.trim(c).toLowerCase();if(e=="transparent"){return f(255,255,255,0)}else{d=a[e]||[0,0,0];return f(d[0],d[1],d[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function(c){function b(av,ai,J,af){var Q=[],O={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{show:null,position:"bottom",mode:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},az=null,ad=null,y=null,H=null,A=null,p=[],aw=[],q={left:0,right:0,top:0,bottom:0},G=0,I=0,h=0,w=0,ak={processOptions:[],processRawData:[],processDatapoints:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},aq=this;aq.setData=aj;aq.setupGrid=t;aq.draw=W;aq.getPlaceholder=function(){return av};aq.getCanvas=function(){return az};aq.getPlotOffset=function(){return q};aq.width=function(){return h};aq.height=function(){return w};aq.offset=function(){var aB=y.offset();aB.left+=q.left;aB.top+=q.top;return aB};aq.getData=function(){return Q};aq.getAxes=function(){var aC={},aB;c.each(p.concat(aw),function(aD,aE){if(aE){aC[aE.direction+(aE.n!=1?aE.n:"")+"axis"]=aE}});return aC};aq.getXAxes=function(){return p};aq.getYAxes=function(){return aw};aq.c2p=C;aq.p2c=ar;aq.getOptions=function(){return O};aq.highlight=x;aq.unhighlight=T;aq.triggerRedrawOverlay=f;aq.pointOffset=function(aB){return{left:parseInt(p[aA(aB,"x")-1].p2c(+aB.x)+q.left),top:parseInt(aw[aA(aB,"y")-1].p2c(+aB.y)+q.top)}};aq.shutdown=ag;aq.resize=function(){B();g(az);g(ad)};aq.hooks=ak;F(aq);Z(J);X();aj(ai);t();W();ah();function an(aD,aB){aB=[aq].concat(aB);for(var aC=0;aC=O.colors.length){aG=0;++aF}}var aH=0,aN;for(aG=0;aGa3.datamax&&a1!=aB){a3.datamax=a1}}c.each(m(),function(a1,a2){a2.datamin=aO;a2.datamax=aI;a2.used=false});for(aU=0;aU0&&aT[aR-aP]!=null&&aT[aR-aP]!=aT[aR]&&aT[aR-aP+1]!=aT[aR+1]){for(aN=0;aNaM){aM=a0}}if(aX.y){if(a0aV){aV=a0}}}}if(aJ.bars.show){var aY=aJ.bars.align=="left"?0:-aJ.bars.barWidth/2;if(aJ.bars.horizontal){aQ+=aY;aV+=aY+aJ.bars.barWidth}else{aK+=aY;aM+=aY+aJ.bars.barWidth}}aF(aJ.xaxis,aK,aM);aF(aJ.yaxis,aQ,aV)}c.each(m(),function(a1,a2){if(a2.datamin==aO){a2.datamin=null}if(a2.datamax==aI){a2.datamax=null}})}function j(aB,aC){var aD=document.createElement("canvas");aD.className=aC;aD.width=G;aD.height=I;if(!aB){c(aD).css({position:"absolute",left:0,top:0})}c(aD).appendTo(av);if(!aD.getContext){aD=window.G_vmlCanvasManager.initElement(aD)}aD.getContext("2d").save();return aD}function B(){G=av.width();I=av.height();if(G<=0||I<=0){throw"Invalid dimensions for plot, width = "+G+", height = "+I}}function g(aC){if(aC.width!=G){aC.width=G}if(aC.height!=I){aC.height=I}var aB=aC.getContext("2d");aB.restore();aB.save()}function X(){var aC,aB=av.children("canvas.base"),aD=av.children("canvas.overlay");if(aB.length==0||aD==0){av.html("");av.css({padding:0});if(av.css("position")=="static"){av.css("position","relative")}B();az=j(true,"base");ad=j(false,"overlay");aC=false}else{az=aB.get(0);ad=aD.get(0);aC=true}H=az.getContext("2d");A=ad.getContext("2d");y=c([ad,az]);if(aC){av.data("plot").shutdown();aq.resize();A.clearRect(0,0,G,I);y.unbind();av.children().not([az,ad]).remove()}av.data("plot",aq)}function ah(){if(O.grid.hoverable){y.mousemove(aa);y.mouseleave(l)}if(O.grid.clickable){y.click(R)}an(ak.bindEvents,[y])}function ag(){if(M){clearTimeout(M)}y.unbind("mousemove",aa);y.unbind("mouseleave",l);y.unbind("click",R);an(ak.shutdown,[y])}function r(aG){function aC(aH){return aH}var aF,aB,aD=aG.options.transform||aC,aE=aG.options.inverseTransform;if(aG.direction=="x"){aF=aG.scale=h/Math.abs(aD(aG.max)-aD(aG.min));aB=Math.min(aD(aG.max),aD(aG.min))}else{aF=aG.scale=w/Math.abs(aD(aG.max)-aD(aG.min));aF=-aF;aB=Math.max(aD(aG.max),aD(aG.min))}if(aD==aC){aG.p2c=function(aH){return(aH-aB)*aF}}else{aG.p2c=function(aH){return(aD(aH)-aB)*aF}}if(!aE){aG.c2p=function(aH){return aB+aH/aF}}else{aG.c2p=function(aH){return aE(aB+aH/aF)}}}function L(aD){var aB=aD.options,aF,aJ=aD.ticks||[],aI=[],aE,aK=aB.labelWidth,aG=aB.labelHeight,aC;function aH(aM,aL){return c('").appendTo(av)}if(aD.direction=="x"){if(aK==null){aK=Math.floor(G/(aJ.length>0?aJ.length:1))}if(aG==null){aI=[];for(aF=0;aF'+aE+"")}}if(aI.length>0){aI.push('
');aC=aH(aI,"width:10000px;");aG=aC.height();aC.remove()}}}else{if(aK==null||aG==null){for(aF=0;aF'+aE+"")}}if(aI.length>0){aC=aH(aI,"");if(aK==null){aK=aC.children().width()}if(aG==null){aG=aC.find("div.tickLabel").height()}aC.remove()}}}if(aK==null){aK=0}if(aG==null){aG=0}aD.labelWidth=aK;aD.labelHeight=aG}function au(aD){var aC=aD.labelWidth,aL=aD.labelHeight,aH=aD.options.position,aF=aD.options.tickLength,aG=O.grid.axisMargin,aJ=O.grid.labelMargin,aK=aD.direction=="x"?p:aw,aE;var aB=c.grep(aK,function(aN){return aN&&aN.options.position==aH&&aN.reserveSpace});if(c.inArray(aD,aB)==aB.length-1){aG=0}if(aF==null){aF="full"}var aI=c.grep(aK,function(aN){return aN&&aN.reserveSpace});var aM=c.inArray(aD,aI)==0;if(!aM&&aF=="full"){aF=5}if(!isNaN(+aF)){aJ+=+aF}if(aD.direction=="x"){aL+=aJ;if(aH=="bottom"){q.bottom+=aL+aG;aD.box={top:I-q.bottom,height:aL}}else{aD.box={top:q.top+aG,height:aL};q.top+=aL+aG}}else{aC+=aJ;if(aH=="left"){aD.box={left:q.left+aG,width:aC};q.left+=aC+aG}else{q.right+=aC+aG;aD.box={left:G-q.right,width:aC}}}aD.position=aH;aD.tickLength=aF;aD.box.padding=aJ;aD.innermost=aM}function U(aB){if(aB.direction=="x"){aB.box.left=q.left;aB.box.width=h}else{aB.box.top=q.top;aB.box.height=w}}function t(){var aC,aE=m();c.each(aE,function(aF,aG){aG.show=aG.options.show;if(aG.show==null){aG.show=aG.used}aG.reserveSpace=aG.show||aG.options.reserveSpace;n(aG)});allocatedAxes=c.grep(aE,function(aF){return aF.reserveSpace});q.left=q.right=q.top=q.bottom=0;if(O.grid.show){c.each(allocatedAxes,function(aF,aG){S(aG);P(aG);ap(aG,aG.ticks);L(aG)});for(aC=allocatedAxes.length-1;aC>=0;--aC){au(allocatedAxes[aC])}var aD=O.grid.minBorderMargin;if(aD==null){aD=0;for(aC=0;aC=0){aD=0}}if(aF.max==null){aB+=aH*aG;if(aB>0&&aE.datamax!=null&&aE.datamax<=0){aB=0}}}}aE.min=aD;aE.max=aB}function S(aG){var aM=aG.options;var aH;if(typeof aM.ticks=="number"&&aM.ticks>0){aH=aM.ticks}else{aH=0.3*Math.sqrt(aG.direction=="x"?G:I)}var aT=(aG.max-aG.min)/aH,aO,aB,aN,aR,aS,aQ,aI;if(aM.mode=="time"){var aJ={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var aK=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var aC=0;if(aM.minTickSize!=null){if(typeof aM.tickSize=="number"){aC=aM.tickSize}else{aC=aM.minTickSize[0]*aJ[aM.minTickSize[1]]}}for(var aS=0;aS=aC){break}}aO=aK[aS][0];aN=aK[aS][1];if(aN=="year"){aQ=Math.pow(10,Math.floor(Math.log(aT/aJ.year)/Math.LN10));aI=(aT/aJ.year)/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ}aG.tickSize=aM.tickSize||[aO,aN];aB=function(aX){var a2=[],a0=aX.tickSize[0],a3=aX.tickSize[1],a1=new Date(aX.min);var aW=a0*aJ[a3];if(a3=="second"){a1.setUTCSeconds(a(a1.getUTCSeconds(),a0))}if(a3=="minute"){a1.setUTCMinutes(a(a1.getUTCMinutes(),a0))}if(a3=="hour"){a1.setUTCHours(a(a1.getUTCHours(),a0))}if(a3=="month"){a1.setUTCMonth(a(a1.getUTCMonth(),a0))}if(a3=="year"){a1.setUTCFullYear(a(a1.getUTCFullYear(),a0))}a1.setUTCMilliseconds(0);if(aW>=aJ.minute){a1.setUTCSeconds(0)}if(aW>=aJ.hour){a1.setUTCMinutes(0)}if(aW>=aJ.day){a1.setUTCHours(0)}if(aW>=aJ.day*4){a1.setUTCDate(1)}if(aW>=aJ.year){a1.setUTCMonth(0)}var a5=0,a4=Number.NaN,aY;do{aY=a4;a4=a1.getTime();a2.push(a4);if(a3=="month"){if(a0<1){a1.setUTCDate(1);var aV=a1.getTime();a1.setUTCMonth(a1.getUTCMonth()+1);var aZ=a1.getTime();a1.setTime(a4+a5*aJ.hour+(aZ-aV)*a0);a5=a1.getUTCHours();a1.setUTCHours(0)}else{a1.setUTCMonth(a1.getUTCMonth()+a0)}}else{if(a3=="year"){a1.setUTCFullYear(a1.getUTCFullYear()+a0)}else{a1.setTime(a4+aW)}}}while(a4aU){aP=aU}aQ=Math.pow(10,-aP);aI=aT/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2;if(aI>2.25&&(aU==null||aP+1<=aU)){aO=2.5;++aP}}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ;if(aM.minTickSize!=null&&aO0){if(aM.min==null){aG.min=Math.min(aG.min,aL[0])}if(aM.max==null&&aL.length>1){aG.max=Math.max(aG.max,aL[aL.length-1])}}aB=function(aX){var aY=[],aV,aW;for(aW=0;aW1&&/\..*0$/.test((aD[1]-aD[0]).toFixed(aE)))){aG.tickDecimals=aE}}}}aG.tickGenerator=aB;if(c.isFunction(aM.tickFormatter)){aG.tickFormatter=function(aV,aW){return""+aM.tickFormatter(aV,aW)}}else{aG.tickFormatter=aR}}function P(aF){var aH=aF.options.ticks,aG=[];if(aH==null||(typeof aH=="number"&&aH>0)){aG=aF.tickGenerator(aF)}else{if(aH){if(c.isFunction(aH)){aG=aH({min:aF.min,max:aF.max})}else{aG=aH}}}var aE,aB;aF.ticks=[];for(aE=0;aE1){aC=aD[1]}}else{aB=+aD}if(aC==null){aC=aF.tickFormatter(aB,aF)}if(!isNaN(aB)){aF.ticks.push({v:aB,label:aC})}}}function ap(aB,aC){if(aB.options.autoscaleMargin&&aC.length>0){if(aB.options.min==null){aB.min=Math.min(aB.min,aC[0].v)}if(aB.options.max==null&&aC.length>1){aB.max=Math.max(aB.max,aC[aC.length-1].v)}}}function W(){H.clearRect(0,0,G,I);var aC=O.grid;if(aC.show&&aC.backgroundColor){N()}if(aC.show&&!aC.aboveData){ac()}for(var aB=0;aBaG){var aC=aH;aH=aG;aG=aC}return{from:aH,to:aG,axis:aE}}function N(){H.save();H.translate(q.left,q.top);H.fillStyle=am(O.grid.backgroundColor,w,0,"rgba(255, 255, 255, 0)");H.fillRect(0,0,h,w);H.restore()}function ac(){var aF;H.save();H.translate(q.left,q.top);var aH=O.grid.markings;if(aH){if(c.isFunction(aH)){var aK=aq.getAxes();aK.xmin=aK.xaxis.min;aK.xmax=aK.xaxis.max;aK.ymin=aK.yaxis.min;aK.ymax=aK.yaxis.max;aH=aH(aK)}for(aF=0;aFaC.axis.max||aI.toaI.axis.max){continue}aC.from=Math.max(aC.from,aC.axis.min);aC.to=Math.min(aC.to,aC.axis.max);aI.from=Math.max(aI.from,aI.axis.min);aI.to=Math.min(aI.to,aI.axis.max);if(aC.from==aC.to&&aI.from==aI.to){continue}aC.from=aC.axis.p2c(aC.from);aC.to=aC.axis.p2c(aC.to);aI.from=aI.axis.p2c(aI.from);aI.to=aI.axis.p2c(aI.to);if(aC.from==aC.to||aI.from==aI.to){H.beginPath();H.strokeStyle=aD.color||O.grid.markingsColor;H.lineWidth=aD.lineWidth||O.grid.markingsLineWidth;H.moveTo(aC.from,aI.from);H.lineTo(aC.to,aI.to);H.stroke()}else{H.fillStyle=aD.color||O.grid.markingsColor;H.fillRect(aC.from,aI.to,aC.to-aC.from,aI.from-aI.to)}}}var aK=m(),aM=O.grid.borderWidth;for(var aE=0;aEaB.max||(aQ=="full"&&aM>0&&(aO==aB.min||aO==aB.max))){continue}if(aB.direction=="x"){aN=aB.p2c(aO);aJ=aQ=="full"?-w:aQ;if(aB.position=="top"){aJ=-aJ}}else{aL=aB.p2c(aO);aP=aQ=="full"?-h:aQ;if(aB.position=="left"){aP=-aP}}if(H.lineWidth==1){if(aB.direction=="x"){aN=Math.floor(aN)+0.5}else{aL=Math.floor(aL)+0.5}}H.moveTo(aN,aL);H.lineTo(aN+aP,aL+aJ)}H.stroke()}if(aM){H.lineWidth=aM;H.strokeStyle=O.grid.borderColor;H.strokeRect(-aM/2,-aM/2,h+aM,w+aM)}H.restore()}function k(){av.find(".tickLabels").remove();var aG=[''];var aJ=m();for(var aD=0;aD
');for(var aE=0;aEaC.max){continue}var aK={},aI;if(aC.direction=="x"){aI="center";aK.left=Math.round(q.left+aC.p2c(aH.v)-aC.labelWidth/2);if(aC.position=="bottom"){aK.top=aF.top+aF.padding}else{aK.bottom=I-(aF.top+aF.height-aF.padding)}}else{aK.top=Math.round(q.top+aC.p2c(aH.v)-aC.labelHeight/2);if(aC.position=="left"){aK.right=G-(aF.left+aF.width-aF.padding);aI="right"}else{aK.left=aF.left+aF.padding;aI="left"}}aK.width=aC.labelWidth;var aB=["position:absolute","text-align:"+aI];for(var aL in aK){aB.push(aL+":"+aK[aL]+"px")}aG.push(''+aH.label+"
")}aG.push(" ")}aG.push("");av.append(aG.join(""))}function d(aB){if(aB.lines.show){at(aB)}if(aB.bars.show){e(aB)}if(aB.points.show){ao(aB)}}function at(aE){function aD(aP,aQ,aI,aU,aT){var aV=aP.points,aJ=aP.pointsize,aN=null,aM=null;H.beginPath();for(var aO=aJ;aO=aR&&aS>aT.max){if(aR>aT.max){continue}aL=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aS=aT.max}else{if(aR>=aS&&aR>aT.max){if(aS>aT.max){continue}aK=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aR=aT.max}}if(aL<=aK&&aL=aK&&aL>aU.max){if(aK>aU.max){continue}aS=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aL=aU.max}else{if(aK>=aL&&aK>aU.max){if(aL>aU.max){continue}aR=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aK=aU.max}}if(aL!=aN||aS!=aM){H.moveTo(aU.p2c(aL)+aQ,aT.p2c(aS)+aI)}aN=aK;aM=aR;H.lineTo(aU.p2c(aK)+aQ,aT.p2c(aR)+aI)}H.stroke()}function aF(aI,aQ,aP){var aW=aI.points,aV=aI.pointsize,aN=Math.min(Math.max(0,aP.min),aP.max),aX=0,aU,aT=false,aM=1,aL=0,aR=0;while(true){if(aV>0&&aX>aW.length+aV){break}aX+=aV;var aZ=aW[aX-aV],aK=aW[aX-aV+aM],aY=aW[aX],aJ=aW[aX+aM];if(aT){if(aV>0&&aZ!=null&&aY==null){aR=aX;aV=-aV;aM=2;continue}if(aV<0&&aX==aL+aV){H.fill();aT=false;aV=-aV;aM=1;aX=aL=aR+aV;continue}}if(aZ==null||aY==null){continue}if(aZ<=aY&&aZ=aY&&aZ>aQ.max){if(aY>aQ.max){continue}aK=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aZ=aQ.max}else{if(aY>=aZ&&aY>aQ.max){if(aZ>aQ.max){continue}aJ=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aY=aQ.max}}if(!aT){H.beginPath();H.moveTo(aQ.p2c(aZ),aP.p2c(aN));aT=true}if(aK>=aP.max&&aJ>=aP.max){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.max));H.lineTo(aQ.p2c(aY),aP.p2c(aP.max));continue}else{if(aK<=aP.min&&aJ<=aP.min){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.min));H.lineTo(aQ.p2c(aY),aP.p2c(aP.min));continue}}var aO=aZ,aS=aY;if(aK<=aJ&&aK=aP.min){aZ=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.min}else{if(aJ<=aK&&aJ=aP.min){aY=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.min}}if(aK>=aJ&&aK>aP.max&&aJ<=aP.max){aZ=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.max}else{if(aJ>=aK&&aJ>aP.max&&aK<=aP.max){aY=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.max}}if(aZ!=aO){H.lineTo(aQ.p2c(aO),aP.p2c(aK))}H.lineTo(aQ.p2c(aZ),aP.p2c(aK));H.lineTo(aQ.p2c(aY),aP.p2c(aJ));if(aY!=aS){H.lineTo(aQ.p2c(aY),aP.p2c(aJ));H.lineTo(aQ.p2c(aS),aP.p2c(aJ))}}}H.save();H.translate(q.left,q.top);H.lineJoin="round";var aG=aE.lines.lineWidth,aB=aE.shadowSize;if(aG>0&&aB>0){H.lineWidth=aB;H.strokeStyle="rgba(0,0,0,0.1)";var aH=Math.PI/18;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/2),Math.cos(aH)*(aG/2+aB/2),aE.xaxis,aE.yaxis);H.lineWidth=aB/2;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/4),Math.cos(aH)*(aG/2+aB/4),aE.xaxis,aE.yaxis)}H.lineWidth=aG;H.strokeStyle=aE.color;var aC=ae(aE.lines,aE.color,0,w);if(aC){H.fillStyle=aC;aF(aE.datapoints,aE.xaxis,aE.yaxis)}if(aG>0){aD(aE.datapoints,0,0,aE.xaxis,aE.yaxis)}H.restore()}function ao(aE){function aH(aN,aM,aU,aK,aS,aT,aQ,aJ){var aR=aN.points,aI=aN.pointsize;for(var aL=0;aLaT.max||aOaQ.max){continue}H.beginPath();aP=aT.p2c(aP);aO=aQ.p2c(aO)+aK;if(aJ=="circle"){H.arc(aP,aO,aM,0,aS?Math.PI:Math.PI*2,false)}else{aJ(H,aP,aO,aM,aS)}H.closePath();if(aU){H.fillStyle=aU;H.fill()}H.stroke()}}H.save();H.translate(q.left,q.top);var aG=aE.points.lineWidth,aC=aE.shadowSize,aB=aE.points.radius,aF=aE.points.symbol;if(aG>0&&aC>0){var aD=aC/2;H.lineWidth=aD;H.strokeStyle="rgba(0,0,0,0.1)";aH(aE.datapoints,aB,null,aD+aD/2,true,aE.xaxis,aE.yaxis,aF);H.strokeStyle="rgba(0,0,0,0.2)";aH(aE.datapoints,aB,null,aD/2,true,aE.xaxis,aE.yaxis,aF)}H.lineWidth=aG;H.strokeStyle=aE.color;aH(aE.datapoints,aB,ae(aE.points,aE.color),0,false,aE.xaxis,aE.yaxis,aF);H.restore()}function E(aN,aM,aV,aI,aQ,aF,aD,aL,aK,aU,aR,aC){var aE,aT,aJ,aP,aG,aB,aO,aH,aS;if(aR){aH=aB=aO=true;aG=false;aE=aV;aT=aN;aP=aM+aI;aJ=aM+aQ;if(aTaL.max||aPaK.max){return}if(aEaL.max){aT=aL.max;aB=false}if(aJaK.max){aP=aK.max;aO=false}aE=aL.p2c(aE);aJ=aK.p2c(aJ);aT=aL.p2c(aT);aP=aK.p2c(aP);if(aD){aU.beginPath();aU.moveTo(aE,aJ);aU.lineTo(aE,aP);aU.lineTo(aT,aP);aU.lineTo(aT,aJ);aU.fillStyle=aD(aJ,aP);aU.fill()}if(aC>0&&(aG||aB||aO||aH)){aU.beginPath();aU.moveTo(aE,aJ+aF);if(aG){aU.lineTo(aE,aP+aF)}else{aU.moveTo(aE,aP+aF)}if(aO){aU.lineTo(aT,aP+aF)}else{aU.moveTo(aT,aP+aF)}if(aB){aU.lineTo(aT,aJ+aF)}else{aU.moveTo(aT,aJ+aF)}if(aH){aU.lineTo(aE,aJ+aF)}else{aU.moveTo(aE,aJ+aF)}aU.stroke()}}function e(aD){function aC(aJ,aI,aL,aG,aK,aN,aM){var aO=aJ.points,aF=aJ.pointsize;for(var aH=0;aH")}aH.push("");aF=true}if(aN){aJ=aN(aJ,aM)}aH.push(''+aJ+" ")}if(aF){aH.push(" ")}if(aH.length==0){return}var aL='";if(O.legend.container!=null){c(O.legend.container).html(aL)}else{var aI="",aC=O.legend.position,aD=O.legend.margin;if(aD[0]==null){aD=[aD,aD]}if(aC.charAt(0)=="n"){aI+="top:"+(aD[1]+q.top)+"px;"}else{if(aC.charAt(0)=="s"){aI+="bottom:"+(aD[1]+q.bottom)+"px;"}}if(aC.charAt(1)=="e"){aI+="right:"+(aD[0]+q.right)+"px;"}else{if(aC.charAt(1)=="w"){aI+="left:"+(aD[0]+q.left)+"px;"}}var aK=c(''+aL.replace('style="','style="position:absolute;'+aI+";")+"
").appendTo(av);if(O.legend.backgroundOpacity!=0){var aG=O.legend.backgroundColor;if(aG==null){aG=O.grid.backgroundColor;if(aG&&typeof aG=="string"){aG=c.color.parse(aG)}else{aG=c.color.extract(aK,"background-color")}aG.a=1;aG=aG.toString()}var aB=aK.children();c('
').prependTo(aK).css("opacity",O.legend.backgroundOpacity)}}}var ab=[],M=null;function K(aI,aG,aD){var aO=O.grid.mouseActiveRadius,a0=aO*aO+1,aY=null,aR=false,aW,aU;for(aW=Q.length-1;aW>=0;--aW){if(!aD(Q[aW])){continue}var aP=Q[aW],aH=aP.xaxis,aF=aP.yaxis,aV=aP.datapoints.points,aT=aP.datapoints.pointsize,aQ=aH.c2p(aI),aN=aF.c2p(aG),aC=aO/aH.scale,aB=aO/aF.scale;if(aH.options.inverseTransform){aC=Number.MAX_VALUE}if(aF.options.inverseTransform){aB=Number.MAX_VALUE}if(aP.lines.show||aP.points.show){for(aU=0;aUaC||aK-aQ<-aC||aJ-aN>aB||aJ-aN<-aB){continue}var aM=Math.abs(aH.p2c(aK)-aI),aL=Math.abs(aF.p2c(aJ)-aG),aS=aM*aM+aL*aL;if(aS=Math.min(aZ,aK)&&aN>=aJ+aE&&aN<=aJ+aX):(aQ>=aK+aE&&aQ<=aK+aX&&aN>=Math.min(aZ,aJ)&&aN<=Math.max(aZ,aJ))){aY=[aW,aU/aT]}}}}if(aY){aW=aY[0];aU=aY[1];aT=Q[aW].datapoints.pointsize;return{datapoint:Q[aW].datapoints.points.slice(aU*aT,(aU+1)*aT),dataIndex:aU,series:Q[aW],seriesIndex:aW}}return null}function aa(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return aC.hoverable!=false})}}function l(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return false})}}function R(aB){u("plotclick",aB,function(aC){return aC.clickable!=false})}function u(aC,aB,aD){var aE=y.offset(),aH=aB.pageX-aE.left-q.left,aF=aB.pageY-aE.top-q.top,aJ=C({left:aH,top:aF});aJ.pageX=aB.pageX;aJ.pageY=aB.pageY;var aK=K(aH,aF,aD);if(aK){aK.pageX=parseInt(aK.series.xaxis.p2c(aK.datapoint[0])+aE.left+q.left);aK.pageY=parseInt(aK.series.yaxis.p2c(aK.datapoint[1])+aE.top+q.top)}if(O.grid.autoHighlight){for(var aG=0;aGaH.max||aIaG.max){return}var aF=aE.points.radius+aE.points.lineWidth/2;A.lineWidth=aF;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aB=1.5*aF,aC=aH.p2c(aC),aI=aG.p2c(aI);A.beginPath();if(aE.points.symbol=="circle"){A.arc(aC,aI,aB,0,2*Math.PI,false)}else{aE.points.symbol(A,aC,aI,aB,false)}A.closePath();A.stroke()}function v(aE,aB){A.lineWidth=aE.bars.lineWidth;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aD=c.color.parse(aE.color).scale("a",0.5).toString();var aC=aE.bars.align=="left"?0:-aE.bars.barWidth/2;E(aB[0],aB[1],aB[2]||0,aC,aC+aE.bars.barWidth,0,function(){return aD},aE.xaxis,aE.yaxis,A,aE.bars.horizontal,aE.bars.lineWidth)}function am(aJ,aB,aH,aC){if(typeof aJ=="string"){return aJ}else{var aI=H.createLinearGradient(0,aH,0,aB);for(var aE=0,aD=aJ.colors.length;aE12){n=n-12}else{if(n==0){n=12}}}for(var g=0;g0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY) max) {
+ // make sure min < max
+ var tmp = min;
+ min = max;
+ max = tmp;
+ }
+
+ var range = max - min;
+ if (zr &&
+ ((zr[0] != null && range < zr[0]) ||
+ (zr[1] != null && range > zr[1])))
+ return;
+
+ opts.min = min;
+ opts.max = max;
+ });
+
+ plot.setupGrid();
+ plot.draw();
+
+ if (!args.preventEvent)
+ plot.getPlaceholder().trigger("plotzoom", [ plot ]);
+ }
+
+ plot.pan = function (args) {
+ var delta = {
+ x: +args.left,
+ y: +args.top
+ };
+
+ if (isNaN(delta.x))
+ delta.x = 0;
+ if (isNaN(delta.y))
+ delta.y = 0;
+
+ $.each(plot.getAxes(), function (_, axis) {
+ var opts = axis.options,
+ min, max, d = delta[axis.direction];
+
+ min = axis.c2p(axis.p2c(axis.min) + d),
+ max = axis.c2p(axis.p2c(axis.max) + d);
+
+ var pr = opts.panRange;
+ if (pr === false) // no panning on this axis
+ return;
+
+ if (pr) {
+ // check whether we hit the wall
+ if (pr[0] != null && pr[0] > min) {
+ d = pr[0] - min;
+ min += d;
+ max += d;
+ }
+
+ if (pr[1] != null && pr[1] < max) {
+ d = pr[1] - max;
+ min += d;
+ max += d;
+ }
+ }
+
+ opts.min = min;
+ opts.max = max;
+ });
+
+ plot.setupGrid();
+ plot.draw();
+
+ if (!args.preventEvent)
+ plot.getPlaceholder().trigger("plotpan", [ plot ]);
+ }
+
+ function shutdown(plot, eventHolder) {
+ eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
+ eventHolder.unbind("mousewheel", onMouseWheel);
+ eventHolder.unbind("dragstart", onDragStart);
+ eventHolder.unbind("drag", onDrag);
+ eventHolder.unbind("dragend", onDragEnd);
+ if (panTimeout)
+ clearTimeout(panTimeout);
+ }
+
+ plot.hooks.bindEvents.push(bindEvents);
+ plot.hooks.shutdown.push(shutdown);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'navigate',
+ version: '1.3'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.navigate.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.navigate.min.js
new file mode 100644
index 0000000000..ecf63c93ba
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.navigate.min.js
@@ -0,0 +1 @@
+(function(i){i.fn.drag=function(j,k,l){if(k){this.bind("dragstart",j)}if(l){this.bind("dragend",l)}return !j?this.trigger("drag"):this.bind("drag",k?k:j)};var d=i.event,c=d.special,h=c.drag={not:":input",distance:0,which:1,dragging:false,setup:function(j){j=i.extend({distance:h.distance,which:h.which,not:h.not},j||{});j.distance=e(j.distance);d.add(this,"mousedown",f,j);if(this.attachEvent){this.attachEvent("ondragstart",a)}},teardown:function(){d.remove(this,"mousedown",f);if(this===h.dragging){h.dragging=h.proxy=false}g(this,true);if(this.detachEvent){this.detachEvent("ondragstart",a)}}};c.dragstart=c.dragend={setup:function(){},teardown:function(){}};function f(j){var k=this,l,m=j.data||{};if(m.elem){k=j.dragTarget=m.elem;j.dragProxy=h.proxy||k;j.cursorOffsetX=m.pageX-m.left;j.cursorOffsetY=m.pageY-m.top;j.offsetX=j.pageX-j.cursorOffsetX;j.offsetY=j.pageY-j.cursorOffsetY}else{if(h.dragging||(m.which>0&&j.which!=m.which)||i(j.target).is(m.not)){return}}switch(j.type){case"mousedown":i.extend(m,i(k).offset(),{elem:k,target:j.target,pageX:j.pageX,pageY:j.pageY});d.add(document,"mousemove mouseup",f,m);g(k,false);h.dragging=null;return false;case !h.dragging&&"mousemove":if(e(j.pageX-m.pageX)+e(j.pageY-m.pageY)w){var A=B;B=w;w=A}var y=w-B;if(E&&((E[0]!=null&&yE[1]))){return}D.min=B;D.max=w});o.setupGrid();o.draw();if(!q.preventEvent){o.getPlaceholder().trigger("plotzoom",[o])}};o.pan=function(p){var q={x:+p.left,y:+p.top};if(isNaN(q.x)){q.x=0}if(isNaN(q.y)){q.y=0}b.each(o.getAxes(),function(s,u){var v=u.options,t,r,w=q[u.direction];t=u.c2p(u.p2c(u.min)+w),r=u.c2p(u.p2c(u.max)+w);var x=v.panRange;if(x===false){return}if(x){if(x[0]!=null&&x[0]>t){w=x[0]-t;t+=w;r+=w}if(x[1]!=null&&x[1]1)
+ options.series.pie.tilt=1;
+ if (options.series.pie.tilt<0)
+ options.series.pie.tilt=0;
+
+ // add processData hook to do transformations on the data
+ plot.hooks.processDatapoints.push(processDatapoints);
+ plot.hooks.drawOverlay.push(drawOverlay);
+
+ // add draw hook
+ plot.hooks.draw.push(draw);
+ }
+ }
+
+ // bind hoverable events
+ function bindEvents(plot, eventHolder)
+ {
+ var options = plot.getOptions();
+
+ if (options.series.pie.show && options.grid.hoverable)
+ eventHolder.unbind('mousemove').mousemove(onMouseMove);
+
+ if (options.series.pie.show && options.grid.clickable)
+ eventHolder.unbind('click').click(onClick);
+ }
+
+
+ // debugging function that prints out an object
+ function alertObject(obj)
+ {
+ var msg = '';
+ function traverse(obj, depth)
+ {
+ if (!depth)
+ depth = 0;
+ for (var i = 0; i < obj.length; ++i)
+ {
+ for (var j=0; jcanvas.width-maxRadius)
+ centerLeft = canvas.width-maxRadius;
+ }
+
+ function fixData(data)
+ {
+ for (var i = 0; i < data.length; ++i)
+ {
+ if (typeof(data[i].data)=='number')
+ data[i].data = [[1,data[i].data]];
+ else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined')
+ {
+ if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined')
+ data[i].label = data[i].data.label; // fix weirdness coming from flot
+ data[i].data = [[1,0]];
+
+ }
+ }
+ return data;
+ }
+
+ function combine(data)
+ {
+ data = fixData(data);
+ calcTotal(data);
+ var combined = 0;
+ var numCombined = 0;
+ var color = options.series.pie.combine.color;
+
+ var newdata = [];
+ for (var i = 0; i < data.length; ++i)
+ {
+ // make sure its a number
+ data[i].data[0][1] = parseFloat(data[i].data[0][1]);
+ if (!data[i].data[0][1])
+ data[i].data[0][1] = 0;
+
+ if (data[i].data[0][1]/total<=options.series.pie.combine.threshold)
+ {
+ combined += data[i].data[0][1];
+ numCombined++;
+ if (!color)
+ color = data[i].color;
+ }
+ else
+ {
+ newdata.push({
+ data: [[1,data[i].data[0][1]]],
+ color: data[i].color,
+ label: data[i].label,
+ angle: (data[i].data[0][1]*(Math.PI*2))/total,
+ percent: (data[i].data[0][1]/total*100)
+ });
+ }
+ }
+ if (numCombined>0)
+ newdata.push({
+ data: [[1,combined]],
+ color: color,
+ label: options.series.pie.combine.label,
+ angle: (combined*(Math.PI*2))/total,
+ percent: (combined/total*100)
+ });
+ return newdata;
+ }
+
+ function draw(plot, newCtx)
+ {
+ if (!target) return; // if no series were passed
+ ctx = newCtx;
+
+ setupPie();
+ var slices = plot.getData();
+
+ var attempts = 0;
+ while (redraw && attempts0)
+ maxRadius *= shrink;
+ attempts += 1;
+ clear();
+ if (options.series.pie.tilt<=0.8)
+ drawShadow();
+ drawPie();
+ }
+ if (attempts >= redrawAttempts) {
+ clear();
+ target.prepend('Could not draw pie with labels contained inside canvas
');
+ }
+
+ if ( plot.setSeries && plot.insertLegend )
+ {
+ plot.setSeries(slices);
+ plot.insertLegend();
+ }
+
+ // we're actually done at this point, just defining internal functions at this point
+
+ function clear()
+ {
+ ctx.clearRect(0,0,canvas.width,canvas.height);
+ target.children().filter('.pieLabel, .pieLabelBackground').remove();
+ }
+
+ function drawShadow()
+ {
+ var shadowLeft = 5;
+ var shadowTop = 15;
+ var edge = 10;
+ var alpha = 0.02;
+
+ // set radius
+ if (options.series.pie.radius>1)
+ var radius = options.series.pie.radius;
+ else
+ var radius = maxRadius * options.series.pie.radius;
+
+ if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/2)-shadowTop || radius<=edge)
+ return; // shadow would be outside canvas, so don't draw it
+
+ ctx.save();
+ ctx.translate(shadowLeft,shadowTop);
+ ctx.globalAlpha = alpha;
+ ctx.fillStyle = '#000';
+
+ // center and rotate to starting position
+ ctx.translate(centerLeft,centerTop);
+ ctx.scale(1, options.series.pie.tilt);
+
+ //radius -= edge;
+ for (var i=1; i<=edge; i++)
+ {
+ ctx.beginPath();
+ ctx.arc(0,0,radius,0,Math.PI*2,false);
+ ctx.fill();
+ radius -= i;
+ }
+
+ ctx.restore();
+ }
+
+ function drawPie()
+ {
+ startAngle = Math.PI*options.series.pie.startAngle;
+
+ // set radius
+ if (options.series.pie.radius>1)
+ var radius = options.series.pie.radius;
+ else
+ var radius = maxRadius * options.series.pie.radius;
+
+ // center and rotate to starting position
+ ctx.save();
+ ctx.translate(centerLeft,centerTop);
+ ctx.scale(1, options.series.pie.tilt);
+ //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
+
+ // draw slices
+ ctx.save();
+ var currentAngle = startAngle;
+ for (var i = 0; i < slices.length; ++i)
+ {
+ slices[i].startAngle = currentAngle;
+ drawSlice(slices[i].angle, slices[i].color, true);
+ }
+ ctx.restore();
+
+ // draw slice outlines
+ ctx.save();
+ ctx.lineWidth = options.series.pie.stroke.width;
+ currentAngle = startAngle;
+ for (var i = 0; i < slices.length; ++i)
+ drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
+ ctx.restore();
+
+ // draw donut hole
+ drawDonutHole(ctx);
+
+ // draw labels
+ if (options.series.pie.label.show)
+ drawLabels();
+
+ // restore to original state
+ ctx.restore();
+
+ function drawSlice(angle, color, fill)
+ {
+ if (angle<=0)
+ return;
+
+ if (fill)
+ ctx.fillStyle = color;
+ else
+ {
+ ctx.strokeStyle = color;
+ ctx.lineJoin = 'round';
+ }
+
+ ctx.beginPath();
+ if (Math.abs(angle - Math.PI*2) > 0.000000001)
+ ctx.moveTo(0,0); // Center of the pie
+ else if ($.browser.msie)
+ angle -= 0.0001;
+ //ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera
+ ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false);
+ ctx.closePath();
+ //ctx.rotate(angle); // This doesn't work properly in Opera
+ currentAngle += angle;
+
+ if (fill)
+ ctx.fill();
+ else
+ ctx.stroke();
+ }
+
+ function drawLabels()
+ {
+ var currentAngle = startAngle;
+
+ // set radius
+ if (options.series.pie.label.radius>1)
+ var radius = options.series.pie.label.radius;
+ else
+ var radius = maxRadius * options.series.pie.label.radius;
+
+ for (var i = 0; i < slices.length; ++i)
+ {
+ if (slices[i].percent >= options.series.pie.label.threshold*100)
+ drawLabel(slices[i], currentAngle, i);
+ currentAngle += slices[i].angle;
+ }
+
+ function drawLabel(slice, startAngle, index)
+ {
+ if (slice.data[0][1]==0)
+ return;
+
+ // format label text
+ var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
+ if (lf)
+ text = lf(slice.label, slice);
+ else
+ text = slice.label;
+ if (plf)
+ text = plf(text, slice);
+
+ var halfAngle = ((startAngle+slice.angle) + startAngle)/2;
+ var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
+ var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
+
+ var html = '' + text + " ";
+ target.append(html);
+ var label = target.children('#pieLabel'+index);
+ var labelTop = (y - label.height()/2);
+ var labelLeft = (x - label.width()/2);
+ label.css('top', labelTop);
+ label.css('left', labelLeft);
+
+ // check to make sure that the label is not outside the canvas
+ if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0)
+ redraw = true;
+
+ if (options.series.pie.label.background.opacity != 0) {
+ // put in the transparent background separately to avoid blended labels and label boxes
+ var c = options.series.pie.label.background.color;
+ if (c == null) {
+ c = slice.color;
+ }
+ var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;';
+ $('
').insertBefore(label).css('opacity', options.series.pie.label.background.opacity);
+ }
+ } // end individual label function
+ } // end drawLabels function
+ } // end drawPie function
+ } // end draw function
+
+ // Placed here because it needs to be accessed from multiple locations
+ function drawDonutHole(layer)
+ {
+ // draw donut hole
+ if(options.series.pie.innerRadius > 0)
+ {
+ // subtract the center
+ layer.save();
+ innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
+ layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color
+ layer.beginPath();
+ layer.fillStyle = options.series.pie.stroke.color;
+ layer.arc(0,0,innerRadius,0,Math.PI*2,false);
+ layer.fill();
+ layer.closePath();
+ layer.restore();
+
+ // add inner stroke
+ layer.save();
+ layer.beginPath();
+ layer.strokeStyle = options.series.pie.stroke.color;
+ layer.arc(0,0,innerRadius,0,Math.PI*2,false);
+ layer.stroke();
+ layer.closePath();
+ layer.restore();
+ // TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
+ }
+ }
+
+ //-- Additional Interactive related functions --
+
+ function isPointInPoly(poly, pt)
+ {
+ for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
+ ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1]))
+ && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0])
+ && (c = !c);
+ return c;
+ }
+
+ function findNearbySlice(mouseX, mouseY)
+ {
+ var slices = plot.getData(),
+ options = plot.getOptions(),
+ radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
+
+ for (var i = 0; i < slices.length; ++i)
+ {
+ var s = slices[i];
+
+ if(s.pie.show)
+ {
+ ctx.save();
+ ctx.beginPath();
+ ctx.moveTo(0,0); // Center of the pie
+ //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
+ ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false);
+ ctx.closePath();
+ x = mouseX-centerLeft;
+ y = mouseY-centerTop;
+ if(ctx.isPointInPath)
+ {
+ if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop))
+ {
+ //alert('found slice!');
+ ctx.restore();
+ return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
+ }
+ }
+ else
+ {
+ // excanvas for IE doesn;t support isPointInPath, this is a workaround.
+ p1X = (radius * Math.cos(s.startAngle));
+ p1Y = (radius * Math.sin(s.startAngle));
+ p2X = (radius * Math.cos(s.startAngle+(s.angle/4)));
+ p2Y = (radius * Math.sin(s.startAngle+(s.angle/4)));
+ p3X = (radius * Math.cos(s.startAngle+(s.angle/2)));
+ p3Y = (radius * Math.sin(s.startAngle+(s.angle/2)));
+ p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5)));
+ p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5)));
+ p5X = (radius * Math.cos(s.startAngle+s.angle));
+ p5Y = (radius * Math.sin(s.startAngle+s.angle));
+ arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]];
+ arrPoint = [x,y];
+ // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
+ if(isPointInPoly(arrPoly, arrPoint))
+ {
+ ctx.restore();
+ return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i};
+ }
+ }
+ ctx.restore();
+ }
+ }
+
+ return null;
+ }
+
+ function onMouseMove(e)
+ {
+ triggerClickHoverEvent('plothover', e);
+ }
+
+ function onClick(e)
+ {
+ triggerClickHoverEvent('plotclick', e);
+ }
+
+ // trigger click or hover event (they send the same parameters so we share their code)
+ function triggerClickHoverEvent(eventname, e)
+ {
+ var offset = plot.offset(),
+ canvasX = parseInt(e.pageX - offset.left),
+ canvasY = parseInt(e.pageY - offset.top),
+ item = findNearbySlice(canvasX, canvasY);
+
+ if (options.grid.autoHighlight)
+ {
+ // clear auto-highlights
+ for (var i = 0; i < highlights.length; ++i)
+ {
+ var h = highlights[i];
+ if (h.auto == eventname && !(item && h.series == item.series))
+ unhighlight(h.series);
+ }
+ }
+
+ // highlight the slice
+ if (item)
+ highlight(item.series, eventname);
+
+ // trigger any hover bind events
+ var pos = { pageX: e.pageX, pageY: e.pageY };
+ target.trigger(eventname, [ pos, item ]);
+ }
+
+ function highlight(s, auto)
+ {
+ if (typeof s == "number")
+ s = series[s];
+
+ var i = indexOfHighlight(s);
+ if (i == -1)
+ {
+ highlights.push({ series: s, auto: auto });
+ plot.triggerRedrawOverlay();
+ }
+ else if (!auto)
+ highlights[i].auto = false;
+ }
+
+ function unhighlight(s)
+ {
+ if (s == null)
+ {
+ highlights = [];
+ plot.triggerRedrawOverlay();
+ }
+
+ if (typeof s == "number")
+ s = series[s];
+
+ var i = indexOfHighlight(s);
+ if (i != -1)
+ {
+ highlights.splice(i, 1);
+ plot.triggerRedrawOverlay();
+ }
+ }
+
+ function indexOfHighlight(s)
+ {
+ for (var i = 0; i < highlights.length; ++i)
+ {
+ var h = highlights[i];
+ if (h.series == s)
+ return i;
+ }
+ return -1;
+ }
+
+ function drawOverlay(plot, octx)
+ {
+ //alert(options.series.pie.radius);
+ var options = plot.getOptions();
+ //alert(options.series.pie.radius);
+
+ var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
+
+ octx.save();
+ octx.translate(centerLeft, centerTop);
+ octx.scale(1, options.series.pie.tilt);
+
+ for (i = 0; i < highlights.length; ++i)
+ drawHighlight(highlights[i].series);
+
+ drawDonutHole(octx);
+
+ octx.restore();
+
+ function drawHighlight(series)
+ {
+ if (series.angle < 0) return;
+
+ //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
+ octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor
+
+ octx.beginPath();
+ if (Math.abs(series.angle - Math.PI*2) > 0.000000001)
+ octx.moveTo(0,0); // Center of the pie
+ octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false);
+ octx.closePath();
+ octx.fill();
+ }
+
+ }
+
+ } // end init (plugin body)
+
+ // define pie specific options and their default values
+ var options = {
+ series: {
+ pie: {
+ show: false,
+ radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
+ innerRadius:0, /* for donut */
+ startAngle: 3/2,
+ tilt: 1,
+ offset: {
+ top: 0,
+ left: 'auto'
+ },
+ stroke: {
+ color: '#FFF',
+ width: 1
+ },
+ label: {
+ show: 'auto',
+ formatter: function(label, slice){
+ return ''+label+' '+Math.round(slice.percent)+'%
';
+ }, // formatter function
+ radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
+ background: {
+ color: null,
+ opacity: 0
+ },
+ threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
+ },
+ combine: {
+ threshold: -1, // percentage at which to combine little slices into one larger slice
+ color: null, // color to give the new slice (auto-generated if null)
+ label: 'Other' // label to give the new slice
+ },
+ highlight: {
+ //color: '#FFF', // will add this functionality once parseColor is available
+ opacity: 0.5
+ }
+ }
+ }
+ };
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: "pie",
+ version: "1.0"
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.pie.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.pie.min.js
new file mode 100644
index 0000000000..b7bf870d75
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.pie.min.js
@@ -0,0 +1 @@
+(function(b){function c(D){var h=null;var L=null;var n=null;var B=null;var p=null;var M=0;var F=true;var o=10;var w=0.95;var A=0;var d=false;var z=false;var j=[];D.hooks.processOptions.push(g);D.hooks.bindEvents.push(e);function g(O,N){if(N.series.pie.show){N.grid.show=false;if(N.series.pie.label.show=="auto"){if(N.legend.show){N.series.pie.label.show=false}else{N.series.pie.label.show=true}}if(N.series.pie.radius=="auto"){if(N.series.pie.label.show){N.series.pie.radius=3/4}else{N.series.pie.radius=1}}if(N.series.pie.tilt>1){N.series.pie.tilt=1}if(N.series.pie.tilt<0){N.series.pie.tilt=0}O.hooks.processDatapoints.push(E);O.hooks.drawOverlay.push(H);O.hooks.draw.push(r)}}function e(P,N){var O=P.getOptions();if(O.series.pie.show&&O.grid.hoverable){N.unbind("mousemove").mousemove(t)}if(O.series.pie.show&&O.grid.clickable){N.unbind("click").click(l)}}function G(O){var P="";function N(S,T){if(!T){T=0}for(var R=0;Rh.width-n){B=h.width-n}}}function v(O){for(var N=0;N0){R.push({data:[[1,P]],color:N,label:a.series.pie.combine.label,angle:(P*(Math.PI*2))/M,percent:(P/M*100)})}return R}function r(S,Q){if(!L){return}ctx=Q;I();var T=S.getData();var P=0;while(F&&P0){n*=w}P+=1;N();if(a.series.pie.tilt<=0.8){O()}R()}if(P>=o){N();L.prepend('Could not draw pie with labels contained inside canvas
')}if(S.setSeries&&S.insertLegend){S.setSeries(T);S.insertLegend()}function N(){ctx.clearRect(0,0,h.width,h.height);L.children().filter(".pieLabel, .pieLabelBackground").remove()}function O(){var Z=5;var Y=15;var W=10;var X=0.02;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}if(U>=(h.width/2)-Z||U*a.series.pie.tilt>=(h.height/2)-Y||U<=W){return}ctx.save();ctx.translate(Z,Y);ctx.globalAlpha=X;ctx.fillStyle="#000";ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);for(var V=1;V<=W;V++){ctx.beginPath();ctx.arc(0,0,U,0,Math.PI*2,false);ctx.fill();U-=V}ctx.restore()}function R(){startAngle=Math.PI*a.series.pie.startAngle;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}ctx.save();ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);ctx.save();var Y=startAngle;for(var W=0;W1e-9){ctx.moveTo(0,0)}else{if(b.browser.msie){ab-=0.0001}}ctx.arc(0,0,U,Y,Y+ab,false);ctx.closePath();Y+=ab;if(aa){ctx.fill()}else{ctx.stroke()}}function V(){var ac=startAngle;if(a.series.pie.label.radius>1){var Z=a.series.pie.label.radius}else{var Z=n*a.series.pie.label.radius}for(var ab=0;ab=a.series.pie.label.threshold*100){aa(T[ab],ac,ab)}ac+=T[ab].angle}function aa(ap,ai,ag){if(ap.data[0][1]==0){return}var ar=a.legend.labelFormatter,aq,ae=a.series.pie.label.formatter;if(ar){aq=ar(ap.label,ap)}else{aq=ap.label}if(ae){aq=ae(aq,ap)}var aj=((ai+ap.angle)+ai)/2;var ao=B+Math.round(Math.cos(aj)*Z);var am=p+Math.round(Math.sin(aj)*Z)*a.series.pie.tilt;var af=''+aq+" ";L.append(af);var an=L.children("#pieLabel"+ag);var ad=(am-an.height()/2);var ah=(ao-an.width()/2);an.css("top",ad);an.css("left",ah);if(0-ad>0||0-ah>0||h.height-(ad+an.height())<0||h.width-(ah+an.width())<0){F=true}if(a.series.pie.label.background.opacity!=0){var ak=a.series.pie.label.background.color;if(ak==null){ak=ap.color}var al="top:"+ad+"px;left:"+ah+"px;";b('
').insertBefore(an).css("opacity",a.series.pie.label.background.opacity)}}}}}function J(N){if(a.series.pie.innerRadius>0){N.save();innerRadius=a.series.pie.innerRadius>1?a.series.pie.innerRadius:n*a.series.pie.innerRadius;N.globalCompositeOperation="destination-out";N.beginPath();N.fillStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.fill();N.closePath();N.restore();N.save();N.beginPath();N.strokeStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.stroke();N.closePath();N.restore()}}function s(Q,R){for(var S=false,P=-1,N=Q.length,O=N-1;++P1?O.series.pie.radius:n*O.series.pie.radius;for(var Q=0;Q1?P.series.pie.radius:n*P.series.pie.radius;R.save();R.translate(B,p);R.scale(1,P.series.pie.tilt);for(i=0;i1e-9){R.moveTo(0,0)}R.arc(0,0,N,S.startAngle,S.startAngle+S.angle,false);R.closePath();R.fill()}}}var a={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,offset:{top:0,left:"auto"},stroke:{color:"#FFF",width:1},label:{show:"auto",formatter:function(d,e){return''+d+" "+Math.round(e.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:0.5}}}};b.plot.plugins.push({init:c,options:a,name:"pie",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.js
new file mode 100644
index 0000000000..69dfb24f38
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.js
@@ -0,0 +1,60 @@
+/*
+Flot plugin for automatically redrawing plots when the placeholder
+size changes, e.g. on window resizes.
+
+It works by listening for changes on the placeholder div (through the
+jQuery resize event plugin) - if the size changes, it will redraw the
+plot.
+
+There are no options. If you need to disable the plugin for some
+plots, you can just fix the size of their placeholders.
+*/
+
+
+/* Inline dependency:
+ * jQuery resize event - v1.1 - 3/14/2010
+ * http://benalman.com/projects/jquery-resize-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);
+
+
+(function ($) {
+ var options = { }; // no options
+
+ function init(plot) {
+ function onResize() {
+ var placeholder = plot.getPlaceholder();
+
+ // somebody might have hidden us and we can't plot
+ // when we don't have the dimensions
+ if (placeholder.width() == 0 || placeholder.height() == 0)
+ return;
+
+ plot.resize();
+ plot.setupGrid();
+ plot.draw();
+ }
+
+ function bindEvents(plot, eventHolder) {
+ plot.getPlaceholder().resize(onResize);
+ }
+
+ function shutdown(plot, eventHolder) {
+ plot.getPlaceholder().unbind("resize", onResize);
+ }
+
+ plot.hooks.bindEvents.push(bindEvents);
+ plot.hooks.shutdown.push(shutdown);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'resize',
+ version: '1.0'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.min.js
new file mode 100644
index 0000000000..1fa0771f57
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.resize.min.js
@@ -0,0 +1 @@
+(function(n,p,u){var w=n([]),s=n.resize=n.extend(n.resize,{}),o,l="setTimeout",m="resize",t=m+"-special-event",v="delay",r="throttleWindow";s[v]=250;s[r]=true;n.event.special[m]={setup:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.add(a);n.data(this,t,{w:a.width(),h:a.height()});if(w.length===1){q()}},teardown:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.not(a);a.removeData(t);if(!w.length){clearTimeout(o)}},add:function(b){if(!s[r]&&this[l]){return false}var c;function a(d,h,g){var f=n(this),e=n.data(this,t);e.w=h!==u?h:f.width();e.h=g!==u?g:f.height();c.apply(this,arguments)}if(n.isFunction(b)){c=b;return a}else{c=b.handler;b.handler=a}}};function q(){o=p[l](function(){w.each(function(){var d=n(this),a=d.width(),b=d.height(),c=n.data(this,t);if(a!==c.w||b!==c.h){d.trigger(m,[c.w=a,c.h=b])}});q()},s[v])}})(jQuery,this);(function(b){var a={};function c(f){function e(){var h=f.getPlaceholder();if(h.width()==0||h.height()==0){return}f.resize();f.setupGrid();f.draw()}function g(i,h){i.getPlaceholder().resize(e)}function d(i,h){i.getPlaceholder().unbind("resize",e)}f.hooks.bindEvents.push(g);f.hooks.shutdown.push(d)}b.plot.plugins.push({init:c,options:a,name:"resize",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.js
new file mode 100644
index 0000000000..7f7b32694b
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.js
@@ -0,0 +1,344 @@
+/*
+Flot plugin for selecting regions.
+
+The plugin defines the following options:
+
+ selection: {
+ mode: null or "x" or "y" or "xy",
+ color: color
+ }
+
+Selection support is enabled by setting the mode to one of "x", "y" or
+"xy". In "x" mode, the user will only be able to specify the x range,
+similarly for "y" mode. For "xy", the selection becomes a rectangle
+where both ranges can be specified. "color" is color of the selection
+(if you need to change the color later on, you can get to it with
+plot.getOptions().selection.color).
+
+When selection support is enabled, a "plotselected" event will be
+emitted on the DOM element you passed into the plot function. The
+event handler gets a parameter with the ranges selected on the axes,
+like this:
+
+ placeholder.bind("plotselected", function(event, ranges) {
+ alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
+ // similar for yaxis - with multiple axes, the extra ones are in
+ // x2axis, x3axis, ...
+ });
+
+The "plotselected" event is only fired when the user has finished
+making the selection. A "plotselecting" event is fired during the
+process with the same parameters as the "plotselected" event, in case
+you want to know what's happening while it's happening,
+
+A "plotunselected" event with no arguments is emitted when the user
+clicks the mouse to remove the selection.
+
+The plugin allso adds the following methods to the plot object:
+
+- setSelection(ranges, preventEvent)
+
+ Set the selection rectangle. The passed in ranges is on the same
+ form as returned in the "plotselected" event. If the selection mode
+ is "x", you should put in either an xaxis range, if the mode is "y"
+ you need to put in an yaxis range and both xaxis and yaxis if the
+ selection mode is "xy", like this:
+
+ setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
+
+ setSelection will trigger the "plotselected" event when called. If
+ you don't want that to happen, e.g. if you're inside a
+ "plotselected" handler, pass true as the second parameter. If you
+ are using multiple axes, you can specify the ranges on any of those,
+ e.g. as x2axis/x3axis/... instead of xaxis, the plugin picks the
+ first one it sees.
+
+- clearSelection(preventEvent)
+
+ Clear the selection rectangle. Pass in true to avoid getting a
+ "plotunselected" event.
+
+- getSelection()
+
+ Returns the current selection in the same format as the
+ "plotselected" event. If there's currently no selection, the
+ function returns null.
+
+*/
+
+(function ($) {
+ function init(plot) {
+ var selection = {
+ first: { x: -1, y: -1}, second: { x: -1, y: -1},
+ show: false,
+ active: false
+ };
+
+ // FIXME: The drag handling implemented here should be
+ // abstracted out, there's some similar code from a library in
+ // the navigation plugin, this should be massaged a bit to fit
+ // the Flot cases here better and reused. Doing this would
+ // make this plugin much slimmer.
+ var savedhandlers = {};
+
+ var mouseUpHandler = null;
+
+ function onMouseMove(e) {
+ if (selection.active) {
+ updateSelection(e);
+
+ plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
+ }
+ }
+
+ function onMouseDown(e) {
+ if (e.which != 1) // only accept left-click
+ return;
+
+ // cancel out any text selections
+ document.body.focus();
+
+ // prevent text selection and drag in old-school browsers
+ if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
+ savedhandlers.onselectstart = document.onselectstart;
+ document.onselectstart = function () { return false; };
+ }
+ if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
+ savedhandlers.ondrag = document.ondrag;
+ document.ondrag = function () { return false; };
+ }
+
+ setSelectionPos(selection.first, e);
+
+ selection.active = true;
+
+ // this is a bit silly, but we have to use a closure to be
+ // able to whack the same handler again
+ mouseUpHandler = function (e) { onMouseUp(e); };
+
+ $(document).one("mouseup", mouseUpHandler);
+ }
+
+ function onMouseUp(e) {
+ mouseUpHandler = null;
+
+ // revert drag stuff for old-school browsers
+ if (document.onselectstart !== undefined)
+ document.onselectstart = savedhandlers.onselectstart;
+ if (document.ondrag !== undefined)
+ document.ondrag = savedhandlers.ondrag;
+
+ // no more dragging
+ selection.active = false;
+ updateSelection(e);
+
+ if (selectionIsSane())
+ triggerSelectedEvent();
+ else {
+ // this counts as a clear
+ plot.getPlaceholder().trigger("plotunselected", [ ]);
+ plot.getPlaceholder().trigger("plotselecting", [ null ]);
+ }
+
+ return false;
+ }
+
+ function getSelection() {
+ if (!selectionIsSane())
+ return null;
+
+ var r = {}, c1 = selection.first, c2 = selection.second;
+ $.each(plot.getAxes(), function (name, axis) {
+ if (axis.used) {
+ var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
+ r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
+ }
+ });
+ return r;
+ }
+
+ function triggerSelectedEvent() {
+ var r = getSelection();
+
+ plot.getPlaceholder().trigger("plotselected", [ r ]);
+
+ // backwards-compat stuff, to be removed in future
+ if (r.xaxis && r.yaxis)
+ plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
+ }
+
+ function clamp(min, value, max) {
+ return value < min ? min: (value > max ? max: value);
+ }
+
+ function setSelectionPos(pos, e) {
+ var o = plot.getOptions();
+ var offset = plot.getPlaceholder().offset();
+ var plotOffset = plot.getPlotOffset();
+ pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
+ pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
+
+ if (o.selection.mode == "y")
+ pos.x = pos == selection.first ? 0 : plot.width();
+
+ if (o.selection.mode == "x")
+ pos.y = pos == selection.first ? 0 : plot.height();
+ }
+
+ function updateSelection(pos) {
+ if (pos.pageX == null)
+ return;
+
+ setSelectionPos(selection.second, pos);
+ if (selectionIsSane()) {
+ selection.show = true;
+ plot.triggerRedrawOverlay();
+ }
+ else
+ clearSelection(true);
+ }
+
+ function clearSelection(preventEvent) {
+ if (selection.show) {
+ selection.show = false;
+ plot.triggerRedrawOverlay();
+ if (!preventEvent)
+ plot.getPlaceholder().trigger("plotunselected", [ ]);
+ }
+ }
+
+ // function taken from markings support in Flot
+ function extractRange(ranges, coord) {
+ var axis, from, to, key, axes = plot.getAxes();
+
+ for (var k in axes) {
+ axis = axes[k];
+ if (axis.direction == coord) {
+ key = coord + axis.n + "axis";
+ if (!ranges[key] && axis.n == 1)
+ key = coord + "axis"; // support x1axis as xaxis
+ if (ranges[key]) {
+ from = ranges[key].from;
+ to = ranges[key].to;
+ break;
+ }
+ }
+ }
+
+ // backwards-compat stuff - to be removed in future
+ if (!ranges[key]) {
+ axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
+ from = ranges[coord + "1"];
+ to = ranges[coord + "2"];
+ }
+
+ // auto-reverse as an added bonus
+ if (from != null && to != null && from > to) {
+ var tmp = from;
+ from = to;
+ to = tmp;
+ }
+
+ return { from: from, to: to, axis: axis };
+ }
+
+ function setSelection(ranges, preventEvent) {
+ var axis, range, o = plot.getOptions();
+
+ if (o.selection.mode == "y") {
+ selection.first.x = 0;
+ selection.second.x = plot.width();
+ }
+ else {
+ range = extractRange(ranges, "x");
+
+ selection.first.x = range.axis.p2c(range.from);
+ selection.second.x = range.axis.p2c(range.to);
+ }
+
+ if (o.selection.mode == "x") {
+ selection.first.y = 0;
+ selection.second.y = plot.height();
+ }
+ else {
+ range = extractRange(ranges, "y");
+
+ selection.first.y = range.axis.p2c(range.from);
+ selection.second.y = range.axis.p2c(range.to);
+ }
+
+ selection.show = true;
+ plot.triggerRedrawOverlay();
+ if (!preventEvent && selectionIsSane())
+ triggerSelectedEvent();
+ }
+
+ function selectionIsSane() {
+ var minSize = 5;
+ return Math.abs(selection.second.x - selection.first.x) >= minSize &&
+ Math.abs(selection.second.y - selection.first.y) >= minSize;
+ }
+
+ plot.clearSelection = clearSelection;
+ plot.setSelection = setSelection;
+ plot.getSelection = getSelection;
+
+ plot.hooks.bindEvents.push(function(plot, eventHolder) {
+ var o = plot.getOptions();
+ if (o.selection.mode != null) {
+ eventHolder.mousemove(onMouseMove);
+ eventHolder.mousedown(onMouseDown);
+ }
+ });
+
+
+ plot.hooks.drawOverlay.push(function (plot, ctx) {
+ // draw selection
+ if (selection.show && selectionIsSane()) {
+ var plotOffset = plot.getPlotOffset();
+ var o = plot.getOptions();
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ var c = $.color.parse(o.selection.color);
+
+ ctx.strokeStyle = c.scale('a', 0.8).toString();
+ ctx.lineWidth = 1;
+ ctx.lineJoin = "round";
+ ctx.fillStyle = c.scale('a', 0.4).toString();
+
+ var x = Math.min(selection.first.x, selection.second.x),
+ y = Math.min(selection.first.y, selection.second.y),
+ w = Math.abs(selection.second.x - selection.first.x),
+ h = Math.abs(selection.second.y - selection.first.y);
+
+ ctx.fillRect(x, y, w, h);
+ ctx.strokeRect(x, y, w, h);
+
+ ctx.restore();
+ }
+ });
+
+ plot.hooks.shutdown.push(function (plot, eventHolder) {
+ eventHolder.unbind("mousemove", onMouseMove);
+ eventHolder.unbind("mousedown", onMouseDown);
+
+ if (mouseUpHandler)
+ $(document).unbind("mouseup", mouseUpHandler);
+ });
+
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: {
+ selection: {
+ mode: null, // one of null, "x", "y" or "xy"
+ color: "#e8cfac"
+ }
+ },
+ name: 'selection',
+ version: '1.1'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.min.js
new file mode 100644
index 0000000000..badc0052db
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.selection.min.js
@@ -0,0 +1 @@
+(function(a){function b(k){var p={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var m={};var r=null;function e(s){if(p.active){l(s);k.getPlaceholder().trigger("plotselecting",[g()])}}function n(s){if(s.which!=1){return}document.body.focus();if(document.onselectstart!==undefined&&m.onselectstart==null){m.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&m.ondrag==null){m.ondrag=document.ondrag;document.ondrag=function(){return false}}d(p.first,s);p.active=true;r=function(t){j(t)};a(document).one("mouseup",r)}function j(s){r=null;if(document.onselectstart!==undefined){document.onselectstart=m.onselectstart}if(document.ondrag!==undefined){document.ondrag=m.ondrag}p.active=false;l(s);if(f()){i()}else{k.getPlaceholder().trigger("plotunselected",[]);k.getPlaceholder().trigger("plotselecting",[null])}return false}function g(){if(!f()){return null}var u={},t=p.first,s=p.second;a.each(k.getAxes(),function(v,w){if(w.used){var y=w.c2p(t[w.direction]),x=w.c2p(s[w.direction]);u[v]={from:Math.min(y,x),to:Math.max(y,x)}}});return u}function i(){var s=g();k.getPlaceholder().trigger("plotselected",[s]);if(s.xaxis&&s.yaxis){k.getPlaceholder().trigger("selected",[{x1:s.xaxis.from,y1:s.yaxis.from,x2:s.xaxis.to,y2:s.yaxis.to}])}}function h(t,u,s){return us?s:u)}function d(w,t){var v=k.getOptions();var u=k.getPlaceholder().offset();var s=k.getPlotOffset();w.x=h(0,t.pageX-u.left-s.left,k.width());w.y=h(0,t.pageY-u.top-s.top,k.height());if(v.selection.mode=="y"){w.x=w==p.first?0:k.width()}if(v.selection.mode=="x"){w.y=w==p.first?0:k.height()}}function l(s){if(s.pageX==null){return}d(p.second,s);if(f()){p.show=true;k.triggerRedrawOverlay()}else{q(true)}}function q(s){if(p.show){p.show=false;k.triggerRedrawOverlay();if(!s){k.getPlaceholder().trigger("plotunselected",[])}}}function c(s,w){var t,y,z,A,x=k.getAxes();for(var u in x){t=x[u];if(t.direction==w){A=w+t.n+"axis";if(!s[A]&&t.n==1){A=w+"axis"}if(s[A]){y=s[A].from;z=s[A].to;break}}}if(!s[A]){t=w=="x"?k.getXAxes()[0]:k.getYAxes()[0];y=s[w+"1"];z=s[w+"2"]}if(y!=null&&z!=null&&y>z){var v=y;y=z;z=v}return{from:y,to:z,axis:t}}function o(t,s){var v,u,w=k.getOptions();if(w.selection.mode=="y"){p.first.x=0;p.second.x=k.width()}else{u=c(t,"x");p.first.x=u.axis.p2c(u.from);p.second.x=u.axis.p2c(u.to)}if(w.selection.mode=="x"){p.first.y=0;p.second.y=k.height()}else{u=c(t,"y");p.first.y=u.axis.p2c(u.from);p.second.y=u.axis.p2c(u.to)}p.show=true;k.triggerRedrawOverlay();if(!s&&f()){i()}}function f(){var s=5;return Math.abs(p.second.x-p.first.x)>=s&&Math.abs(p.second.y-p.first.y)>=s}k.clearSelection=q;k.setSelection=o;k.getSelection=g;k.hooks.bindEvents.push(function(t,s){var u=t.getOptions();if(u.selection.mode!=null){s.mousemove(e);s.mousedown(n)}});k.hooks.drawOverlay.push(function(v,D){if(p.show&&f()){var t=v.getPlotOffset();var s=v.getOptions();D.save();D.translate(t.left,t.top);var z=a.color.parse(s.selection.color);D.strokeStyle=z.scale("a",0.8).toString();D.lineWidth=1;D.lineJoin="round";D.fillStyle=z.scale("a",0.4).toString();var B=Math.min(p.first.x,p.second.x),A=Math.min(p.first.y,p.second.y),C=Math.abs(p.second.x-p.first.x),u=Math.abs(p.second.y-p.first.y);D.fillRect(B,A,C,u);D.strokeRect(B,A,C,u);D.restore()}});k.hooks.shutdown.push(function(t,s){s.unbind("mousemove",e);s.unbind("mousedown",n);if(r){a(document).unbind("mouseup",r)}})}a.plot.plugins.push({init:b,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.1"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.js
new file mode 100644
index 0000000000..a31d5dc9b5
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.js
@@ -0,0 +1,184 @@
+/*
+Flot plugin for stacking data sets, i.e. putting them on top of each
+other, for accumulative graphs.
+
+The plugin assumes the data is sorted on x (or y if stacking
+horizontally). For line charts, it is assumed that if a line has an
+undefined gap (from a null point), then the line above it should have
+the same gap - insert zeros instead of "null" if you want another
+behaviour. This also holds for the start and end of the chart. Note
+that stacking a mix of positive and negative values in most instances
+doesn't make sense (so it looks weird).
+
+Two or more series are stacked when their "stack" attribute is set to
+the same key (which can be any number or string or just "true"). To
+specify the default stack, you can set
+
+ series: {
+ stack: null or true or key (number/string)
+ }
+
+or specify it for a specific series
+
+ $.plot($("#placeholder"), [{ data: [ ... ], stack: true }])
+
+The stacking order is determined by the order of the data series in
+the array (later series end up on top of the previous).
+
+Internally, the plugin modifies the datapoints in each series, adding
+an offset to the y value. For line series, extra data points are
+inserted through interpolation. If there's a second y value, it's also
+adjusted (e.g for bar charts or filled areas).
+*/
+
+(function ($) {
+ var options = {
+ series: { stack: null } // or number/string
+ };
+
+ function init(plot) {
+ function findMatchingSeries(s, allseries) {
+ var res = null
+ for (var i = 0; i < allseries.length; ++i) {
+ if (s == allseries[i])
+ break;
+
+ if (allseries[i].stack == s.stack)
+ res = allseries[i];
+ }
+
+ return res;
+ }
+
+ function stackData(plot, s, datapoints) {
+ if (s.stack == null)
+ return;
+
+ var other = findMatchingSeries(s, plot.getData());
+ if (!other)
+ return;
+
+ var ps = datapoints.pointsize,
+ points = datapoints.points,
+ otherps = other.datapoints.pointsize,
+ otherpoints = other.datapoints.points,
+ newpoints = [],
+ px, py, intery, qx, qy, bottom,
+ withlines = s.lines.show,
+ horizontal = s.bars.horizontal,
+ withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
+ withsteps = withlines && s.lines.steps,
+ fromgap = true,
+ keyOffset = horizontal ? 1 : 0,
+ accumulateOffset = horizontal ? 0 : 1,
+ i = 0, j = 0, l;
+
+ while (true) {
+ if (i >= points.length)
+ break;
+
+ l = newpoints.length;
+
+ if (points[i] == null) {
+ // copy gaps
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ i += ps;
+ }
+ else if (j >= otherpoints.length) {
+ // for lines, we can't use the rest of the points
+ if (!withlines) {
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ }
+ i += ps;
+ }
+ else if (otherpoints[j] == null) {
+ // oops, got a gap
+ for (m = 0; m < ps; ++m)
+ newpoints.push(null);
+ fromgap = true;
+ j += otherps;
+ }
+ else {
+ // cases where we actually got two points
+ px = points[i + keyOffset];
+ py = points[i + accumulateOffset];
+ qx = otherpoints[j + keyOffset];
+ qy = otherpoints[j + accumulateOffset];
+ bottom = 0;
+
+ if (px == qx) {
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+
+ newpoints[l + accumulateOffset] += qy;
+ bottom = qy;
+
+ i += ps;
+ j += otherps;
+ }
+ else if (px > qx) {
+ // we got past point below, might need to
+ // insert interpolated extra point
+ if (withlines && i > 0 && points[i - ps] != null) {
+ intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
+ newpoints.push(qx);
+ newpoints.push(intery + qy);
+ for (m = 2; m < ps; ++m)
+ newpoints.push(points[i + m]);
+ bottom = qy;
+ }
+
+ j += otherps;
+ }
+ else { // px < qx
+ if (fromgap && withlines) {
+ // if we come from a gap, we just skip this point
+ i += ps;
+ continue;
+ }
+
+ for (m = 0; m < ps; ++m)
+ newpoints.push(points[i + m]);
+
+ // we might be able to interpolate a point below,
+ // this can give us a better y
+ if (withlines && j > 0 && otherpoints[j - otherps] != null)
+ bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
+
+ newpoints[l + accumulateOffset] += bottom;
+
+ i += ps;
+ }
+
+ fromgap = false;
+
+ if (l != newpoints.length && withbottom)
+ newpoints[l + 2] += bottom;
+ }
+
+ // maintain the line steps invariant
+ if (withsteps && l != newpoints.length && l > 0
+ && newpoints[l] != null
+ && newpoints[l] != newpoints[l - ps]
+ && newpoints[l + 1] != newpoints[l - ps + 1]) {
+ for (m = 0; m < ps; ++m)
+ newpoints[l + ps + m] = newpoints[l + m];
+ newpoints[l + 1] = newpoints[l - ps + 1];
+ }
+ }
+
+ datapoints.points = newpoints;
+ }
+
+ plot.hooks.processDatapoints.push(stackData);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'stack',
+ version: '1.2'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.min.js
new file mode 100644
index 0000000000..bba2a0e5ff
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.stack.min.js
@@ -0,0 +1 @@
+(function(b){var a={series:{stack:null}};function c(f){function d(k,j){var h=null;for(var g=0;g2&&(G?g.format[2].x:g.format[2].y),n=u&&v.lines.steps,E=true,q=G?1:0,H=G?0:1,D=0,B=0,A;while(true){if(D>=F.length){break}A=t.length;if(F[D]==null){for(m=0;m=y.length){if(!u){for(m=0;mJ){if(u&&D>0&&F[D-z]!=null){k=w+(F[D-z+H]-w)*(J-x)/(F[D-z+q]-x);t.push(J);t.push(k+I);for(m=2;m0&&y[B-h]!=null){r=I+(y[B-h+H]-I)*(x-J)/(y[B-h+q]-J)}t[A+H]+=r;D+=z}}E=false;if(A!=t.length&&o){t[A+2]+=r}}}}if(n&&A!=t.length&&A>0&&t[A]!=null&&t[A]!=t[A-z]&&t[A+1]!=t[A-z+1]){for(m=0;m s = r * sqrt(pi)/2
+ var size = radius * Math.sqrt(Math.PI) / 2;
+ ctx.rect(x - size, y - size, size + size, size + size);
+ },
+ diamond: function (ctx, x, y, radius, shadow) {
+ // pi * r^2 = 2s^2 => s = r * sqrt(pi/2)
+ var size = radius * Math.sqrt(Math.PI / 2);
+ ctx.moveTo(x - size, y);
+ ctx.lineTo(x, y - size);
+ ctx.lineTo(x + size, y);
+ ctx.lineTo(x, y + size);
+ ctx.lineTo(x - size, y);
+ },
+ triangle: function (ctx, x, y, radius, shadow) {
+ // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3))
+ var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3));
+ var height = size * Math.sin(Math.PI / 3);
+ ctx.moveTo(x - size/2, y + height/2);
+ ctx.lineTo(x + size/2, y + height/2);
+ if (!shadow) {
+ ctx.lineTo(x, y - height/2);
+ ctx.lineTo(x - size/2, y + height/2);
+ }
+ },
+ cross: function (ctx, x, y, radius, shadow) {
+ // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
+ var size = radius * Math.sqrt(Math.PI) / 2;
+ ctx.moveTo(x - size, y - size);
+ ctx.lineTo(x + size, y + size);
+ ctx.moveTo(x - size, y + size);
+ ctx.lineTo(x + size, y - size);
+ }
+ }
+
+ var s = series.points.symbol;
+ if (handlers[s])
+ series.points.symbol = handlers[s];
+ }
+
+ function init(plot) {
+ plot.hooks.processDatapoints.push(processRawData);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ name: 'symbols',
+ version: '1.0'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.symbol.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.symbol.min.js
new file mode 100644
index 0000000000..272e003ab4
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.symbol.min.js
@@ -0,0 +1 @@
+(function(b){function a(h,e,g){var d={square:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.rect(j-l,n-l,l+l,l+l)},diamond:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI/2);k.moveTo(j-l,n);k.lineTo(j,n-l);k.lineTo(j+l,n);k.lineTo(j,n+l);k.lineTo(j-l,n)},triangle:function(l,k,o,j,n){var m=j*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3));var i=m*Math.sin(Math.PI/3);l.moveTo(k-m/2,o+i/2);l.lineTo(k+m/2,o+i/2);if(!n){l.lineTo(k,o-i/2);l.lineTo(k-m/2,o+i/2)}},cross:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.moveTo(j-l,n-l);k.lineTo(j+l,n+l);k.moveTo(j-l,n+l);k.lineTo(j+l,n-l)}};var f=e.points.symbol;if(d[f]){e.points.symbol=d[f]}}function c(d){d.hooks.processDatapoints.push(a)}b.plot.plugins.push({init:c,name:"symbols",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.js
new file mode 100644
index 0000000000..0b2e7ac82a
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.js
@@ -0,0 +1,103 @@
+/*
+Flot plugin for thresholding data. Controlled through the option
+"threshold" in either the global series options
+
+ series: {
+ threshold: {
+ below: number
+ color: colorspec
+ }
+ }
+
+or in a specific series
+
+ $.plot($("#placeholder"), [{ data: [ ... ], threshold: { ... }}])
+
+The data points below "below" are drawn with the specified color. This
+makes it easy to mark points below 0, e.g. for budget data.
+
+Internally, the plugin works by splitting the data into two series,
+above and below the threshold. The extra series below the threshold
+will have its label cleared and the special "originSeries" attribute
+set to the original series. You may need to check for this in hover
+events.
+*/
+
+(function ($) {
+ var options = {
+ series: { threshold: null } // or { below: number, color: color spec}
+ };
+
+ function init(plot) {
+ function thresholdData(plot, s, datapoints) {
+ if (!s.threshold)
+ return;
+
+ var ps = datapoints.pointsize, i, x, y, p, prevp,
+ thresholded = $.extend({}, s); // note: shallow copy
+
+ thresholded.datapoints = { points: [], pointsize: ps };
+ thresholded.label = null;
+ thresholded.color = s.threshold.color;
+ thresholded.threshold = null;
+ thresholded.originSeries = s;
+ thresholded.data = [];
+
+ var below = s.threshold.below,
+ origpoints = datapoints.points,
+ addCrossingPoints = s.lines.show;
+
+ threspoints = [];
+ newpoints = [];
+
+ for (i = 0; i < origpoints.length; i += ps) {
+ x = origpoints[i]
+ y = origpoints[i + 1];
+
+ prevp = p;
+ if (y < below)
+ p = threspoints;
+ else
+ p = newpoints;
+
+ if (addCrossingPoints && prevp != p && x != null
+ && i > 0 && origpoints[i - ps] != null) {
+ var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x;
+ prevp.push(interx);
+ prevp.push(below);
+ for (m = 2; m < ps; ++m)
+ prevp.push(origpoints[i + m]);
+
+ p.push(null); // start new segment
+ p.push(null);
+ for (m = 2; m < ps; ++m)
+ p.push(origpoints[i + m]);
+ p.push(interx);
+ p.push(below);
+ for (m = 2; m < ps; ++m)
+ p.push(origpoints[i + m]);
+ }
+
+ p.push(x);
+ p.push(y);
+ }
+
+ datapoints.points = newpoints;
+ thresholded.datapoints.points = threspoints;
+
+ if (thresholded.datapoints.points.length > 0)
+ plot.getData().push(thresholded);
+
+ // FIXME: there are probably some edge cases left in bars
+ }
+
+ plot.hooks.processDatapoints.push(thresholdData);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'threshold',
+ version: '1.0'
+ });
+})(jQuery);
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.min.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.min.js
new file mode 100644
index 0000000000..d8b79dfc93
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.flot/jquery.flot.threshold.min.js
@@ -0,0 +1 @@
+(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.jeditable.mini.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.jeditable.mini.js
new file mode 100644
index 0000000000..ef885f0602
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/jquery.jeditable.mini.js
@@ -0,0 +1,38 @@
+
+(function($){$.fn.editable=function(target,options){if('disable'==target){$(this).data('disabled.editable',true);return;}
+if('enable'==target){$(this).data('disabled.editable',false);return;}
+if('destroy'==target){$(this).unbind($(this).data('event.editable')).removeData('disabled.editable').removeData('event.editable');return;}
+var settings=$.extend({},$.fn.editable.defaults,{target:target},options);var plugin=$.editable.types[settings.type].plugin||function(){};var submit=$.editable.types[settings.type].submit||function(){};var buttons=$.editable.types[settings.type].buttons||$.editable.types['defaults'].buttons;var content=$.editable.types[settings.type].content||$.editable.types['defaults'].content;var element=$.editable.types[settings.type].element||$.editable.types['defaults'].element;var reset=$.editable.types[settings.type].reset||$.editable.types['defaults'].reset;var callback=settings.callback||function(){};var onedit=settings.onedit||function(){};var onsubmit=settings.onsubmit||function(){};var onreset=settings.onreset||function(){};var onerror=settings.onerror||reset;if(settings.tooltip){$(this).attr('title',settings.tooltip);}
+settings.autowidth='auto'==settings.width;settings.autoheight='auto'==settings.height;return this.each(function(){var self=this;var savedwidth=$(self).width();var savedheight=$(self).height();$(this).data('event.editable',settings.event);if(!$.trim($(this).html())){$(this).html(settings.placeholder);}
+$(this).bind(settings.event,function(e){if(true===$(this).data('disabled.editable')){return;}
+if(self.editing){return;}
+if(false===onedit.apply(this,[settings,self])){return;}
+e.preventDefault();e.stopPropagation();if(settings.tooltip){$(self).removeAttr('title');}
+if(0==$(self).width()){settings.width=savedwidth;settings.height=savedheight;}else{if(settings.width!='none'){settings.width=settings.autowidth?$(self).width():settings.width;}
+if(settings.height!='none'){settings.height=settings.autoheight?$(self).height():settings.height;}}
+if($(this).html().toLowerCase().replace(/(;|")/g,'')==settings.placeholder.toLowerCase().replace(/(;|")/g,'')){$(this).html('');}
+self.editing=true;self.revert=$(self).html();$(self).html('');var form=$('');if(settings.cssclass){if('inherit'==settings.cssclass){form.attr('class',$(self).attr('class'));}else{form.attr('class',settings.cssclass);}}
+if(settings.style){if('inherit'==settings.style){form.attr('style',$(self).attr('style'));form.css('display',$(self).css('display'));}else{form.attr('style',settings.style);}}
+var input=element.apply(form,[settings,self]);var input_content;if(settings.loadurl){var t=setTimeout(function(){input.disabled=true;content.apply(form,[settings.loadtext,settings,self]);},100);var loaddata={};loaddata[settings.id]=self.id;if($.isFunction(settings.loaddata)){$.extend(loaddata,settings.loaddata.apply(self,[self.revert,settings]));}else{$.extend(loaddata,settings.loaddata);}
+$.ajax({type:settings.loadtype,url:settings.loadurl,data:loaddata,async:false,success:function(result){window.clearTimeout(t);input_content=result;input.disabled=false;}});}else if(settings.data){input_content=settings.data;if($.isFunction(settings.data)){input_content=settings.data.apply(self,[self.revert,settings]);}}else{input_content=self.revert;}
+content.apply(form,[input_content,settings,self]);input.attr('name',settings.name);buttons.apply(form,[settings,self]);$(self).append(form);plugin.apply(form,[settings,self]);$(':input:visible:enabled:first',form).focus();if(settings.select){input.select();}
+input.keydown(function(e){if(e.keyCode==27){e.preventDefault();reset.apply(form,[settings,self]);}});var t;if('cancel'==settings.onblur){input.blur(function(e){t=setTimeout(function(){reset.apply(form,[settings,self]);},500);});}else if('submit'==settings.onblur){input.blur(function(e){t=setTimeout(function(){form.submit();},200);});}else if($.isFunction(settings.onblur)){input.blur(function(e){settings.onblur.apply(self,[input.val(),settings]);});}else{input.blur(function(e){});}
+form.submit(function(e){if(t){clearTimeout(t);}
+e.preventDefault();if(false!==onsubmit.apply(form,[settings,self])){if(false!==submit.apply(form,[settings,self])){if($.isFunction(settings.target)){var str=settings.target.apply(self,[input.val(),settings]);$(self).html(str);self.editing=false;callback.apply(self,[self.innerHTML,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}}else{var submitdata={};submitdata[settings.name]=input.val();submitdata[settings.id]=self.id;if($.isFunction(settings.submitdata)){$.extend(submitdata,settings.submitdata.apply(self,[self.revert,settings]));}else{$.extend(submitdata,settings.submitdata);}
+if('PUT'==settings.method){submitdata['_method']='put';}
+$(self).html(settings.indicator);var ajaxoptions={type:'POST',data:submitdata,dataType:'html',url:settings.target,success:function(result,status){if(ajaxoptions.dataType=='html'){$(self).html(result);}
+self.editing=false;callback.apply(self,[result,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}},error:function(xhr,status,error){onerror.apply(form,[settings,self,xhr]);}};$.extend(ajaxoptions,settings.ajaxoptions);$.ajax(ajaxoptions);}}}
+$(self).attr('title',settings.tooltip);return false;});});this.reset=function(form){if(this.editing){if(false!==onreset.apply(form,[settings,self])){$(self).html(self.revert);self.editing=false;if(!$.trim($(self).html())){$(self).html(settings.placeholder);}
+if(settings.tooltip){$(self).attr('title',settings.tooltip);}}}};});};$.editable={types:{defaults:{element:function(settings,original){var input=$(' ');$(this).append(input);return(input);},content:function(string,settings,original){$(':input:first',this).val(string);},reset:function(settings,original){original.reset(this);},buttons:function(settings,original){var form=this;if(settings.submit){if(settings.submit.match(/>$/)){var submit=$(settings.submit).click(function(){if(submit.attr("type")!="submit"){form.submit();}});}else{var submit=$(' ');submit.html(settings.submit);}
+$(this).append(submit);}
+if(settings.cancel){if(settings.cancel.match(/>$/)){var cancel=$(settings.cancel);}else{var cancel=$(' ');cancel.html(settings.cancel);}
+$(this).append(cancel);$(cancel).click(function(event){if($.isFunction($.editable.types[settings.type].reset)){var reset=$.editable.types[settings.type].reset;}else{var reset=$.editable.types['defaults'].reset;}
+reset.apply(form,[settings,original]);return false;});}}},text:{element:function(settings,original){var input=$(' ');if(settings.width!='none'){input.width(settings.width);}
+if(settings.height!='none'){input.height(settings.height);}
+input.attr('autocomplete','off');$(this).append(input);return(input);}},textarea:{element:function(settings,original){var textarea=$('');if(settings.rows){textarea.attr('rows',settings.rows);}else if(settings.height!="none"){textarea.height(settings.height);}
+if(settings.cols){textarea.attr('cols',settings.cols);}else if(settings.width!="none"){textarea.width(settings.width);}
+$(this).append(textarea);return(textarea);}},select:{element:function(settings,original){var select=$(' ');$(this).append(select);return(select);},content:function(data,settings,original){if(String==data.constructor){eval('var json = '+data);}else{var json=data;}
+for(var key in json){if(!json.hasOwnProperty(key)){continue;}
+if('selected'==key){continue;}
+var option=$(' ').val(key).append(json[key]);$('select',this).append(option);}
+$('select',this).children().each(function(){if($(this).val()==json['selected']||$(this).text()==$.trim(original.revert)){$(this).attr('selected','selected');}});}}},addInputType:function(name,input){$.editable.types[name]=input;}};$.fn.editable.defaults={name:'value',id:'id',type:'text',width:'auto',height:'auto',event:'click.editable',onblur:'cancel',loadtype:'GET',loadtext:'Loading...',placeholder:'Click to edit',loaddata:{},submitdata:{},ajaxoptions:{}};})(jQuery);
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/MIT-LICENSE b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/MIT-LICENSE
new file mode 100644
index 0000000000..91c62fb8f6
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 PersonalGrid Corporation (http://personalgrid.com/)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/README.md b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/README.md
new file mode 100644
index 0000000000..a6ab512116
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/README.md
@@ -0,0 +1,17 @@
+LivePipe UI is a suite of high quality widgets and controls for web 2.0 applications built using the [Prototype JavaScript Framework](http://prototypejs.org/). Each control is well tested, highly extensible, fully documented and degrades gracefully for non JavaScript enabled browsers where possible. [MIT licensed](http://www.opensource.org/licenses/mit-license.php) and actively maintained.
+
+For more information see [LivePipe.net](http://livepipe.net/)
+
+Contributors
+============
+
+LivePipe UI was created by Ryan Johnson and continues to be mantained and enhanced by many others, including the following:
+
+* Nathan L Smith (nlloyds@gmail.com)
+* Oliver Beddows
+* Blane Dabney
+* Cory Dodt
+* Stuart Halloway
+* Stephen Heuer
+* Ed Sanders
+* Tjoekbezoer van Damme
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/livepipe.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/livepipe.js
new file mode 100644
index 0000000000..0d7197e1ce
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/livepipe.js
@@ -0,0 +1,181 @@
+/**
+ * @author Ryan Johnson
+ * @copyright 2008 PersonalGrid Corporation
+ * @package LivePipe UI
+ * @license MIT
+ * @url http://livepipe.net/core
+ * @require prototype.js
+ */
+
+if(typeof(Control) == 'undefined')
+ Control = {};
+
+var $proc = function(proc){
+ return typeof(proc) == 'function' ? proc : function(){return proc};
+};
+
+var $value = function(value){
+ return typeof(value) == 'function' ? value() : value;
+};
+
+Object.Event = {
+ extend: function(object){
+ object._objectEventSetup = function(event_name){
+ this._observers = this._observers || {};
+ this._observers[event_name] = this._observers[event_name] || [];
+ };
+ object.observe = function(event_name,observer){
+ if(typeof(event_name) == 'string' && typeof(observer) != 'undefined'){
+ this._objectEventSetup(event_name);
+ if(!this._observers[event_name].include(observer))
+ this._observers[event_name].push(observer);
+ }else
+ for(var e in event_name)
+ this.observe(e,event_name[e]);
+ };
+ object.stopObserving = function(event_name,observer){
+ this._objectEventSetup(event_name);
+ if(event_name && observer)
+ this._observers[event_name] = this._observers[event_name].without(observer);
+ else if(event_name)
+ this._observers[event_name] = [];
+ else
+ this._observers = {};
+ };
+ object.observeOnce = function(event_name,outer_observer){
+ var inner_observer = function(){
+ outer_observer.apply(this,arguments);
+ this.stopObserving(event_name,inner_observer);
+ }.bind(this);
+ this._objectEventSetup(event_name);
+ this._observers[event_name].push(inner_observer);
+ };
+ object.notify = function(event_name){
+ this._objectEventSetup(event_name);
+ var collected_return_values = [];
+ var args = $A(arguments).slice(1);
+ try{
+ for(var i = 0; i < this._observers[event_name].length; ++i)
+ collected_return_values.push(this._observers[event_name][i].apply(this,args) || null);
+ }catch(e){
+ if(e == $break)
+ return false;
+ else
+ throw e;
+ }
+ return collected_return_values;
+ };
+ if(object.prototype){
+ object.prototype._objectEventSetup = object._objectEventSetup;
+ object.prototype.observe = object.observe;
+ object.prototype.stopObserving = object.stopObserving;
+ object.prototype.observeOnce = object.observeOnce;
+ object.prototype.notify = function(event_name){
+ if(object.notify){
+ var args = $A(arguments).slice(1);
+ args.unshift(this);
+ args.unshift(event_name);
+ object.notify.apply(object,args);
+ }
+ this._objectEventSetup(event_name);
+ var args = $A(arguments).slice(1);
+ var collected_return_values = [];
+ try{
+ if(this.options && this.options[event_name] && typeof(this.options[event_name]) == 'function')
+ collected_return_values.push(this.options[event_name].apply(this,args) || null);
+ var callbacks_copy = this._observers[event_name]; // since original array will be modified after observeOnce calls
+ for(var i = 0; i < callbacks_copy.length; ++i)
+ collected_return_values.push(callbacks_copy[i].apply(this,args) || null);
+ }catch(e){
+ if(e == $break)
+ return false;
+ else
+ throw e;
+ }
+ return collected_return_values;
+ };
+ }
+ }
+};
+
+/* Begin Core Extensions */
+
+//Element.observeOnce
+Element.addMethods({
+ observeOnce: function(element,event_name,outer_callback){
+ var inner_callback = function(){
+ outer_callback.apply(this,arguments);
+ Element.stopObserving(element,event_name,inner_callback);
+ };
+ Element.observe(element,event_name,inner_callback);
+ }
+});
+
+//mouse:wheel
+(function(){
+ function wheel(event){
+ var delta, element, custom_event;
+ // normalize the delta
+ if (event.wheelDelta) { // IE & Opera
+ delta = event.wheelDelta / 120;
+ } else if (event.detail) { // W3C
+ delta =- event.detail / 3;
+ }
+ if (!delta) { return; }
+ element = Event.extend(event).target;
+ element = Element.extend(element.nodeType === Node.TEXT_NODE ? element.parentNode : element);
+ custom_event = element.fire('mouse:wheel',{ delta: delta });
+ if (custom_event.stopped) {
+ Event.stop(event);
+ return false;
+ }
+ }
+ document.observe('mousewheel',wheel);
+ document.observe('DOMMouseScroll',wheel);
+})();
+
+/* End Core Extensions */
+
+//from PrototypeUI
+var IframeShim = Class.create({
+ initialize: function() {
+ this.element = new Element('iframe',{
+ style: 'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);display:none',
+ src: 'javascript:void(0);',
+ frameborder: 0
+ });
+ $(document.body).insert(this.element);
+ },
+ hide: function() {
+ this.element.hide();
+ return this;
+ },
+ show: function() {
+ this.element.show();
+ return this;
+ },
+ positionUnder: function(element) {
+ var element = $(element);
+ var offset = element.cumulativeOffset();
+ var dimensions = element.getDimensions();
+ this.element.setStyle({
+ left: offset[0] + 'px',
+ top: offset[1] + 'px',
+ width: dimensions.width + 'px',
+ height: dimensions.height + 'px',
+ zIndex: element.getStyle('zIndex') - 1
+ }).show();
+ return this;
+ },
+ setBounds: function(bounds) {
+ for(prop in bounds)
+ bounds[prop] += 'px';
+ this.element.setStyle(bounds);
+ return this;
+ },
+ destroy: function() {
+ if(this.element)
+ this.element.remove();
+ return this;
+ }
+});
diff --git a/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/window.js b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/window.js
new file mode 100644
index 0000000000..b7714e4735
--- /dev/null
+++ b/vendored-plugins/openproject-backlogs/vendor/assets/javascripts/backlogs/livepipe-ui/window.js
@@ -0,0 +1,957 @@
+/**
+ * @author Ryan Johnson
+ * @copyright 2008 PersonalGrid Corporation
+ * @package LivePipe UI
+ * @license MIT
+ * @url http://livepipe.net/control/window
+ * @require prototype.js, effects.js, draggable.js, resizable.js, livepipe.js
+ */
+
+//adds onDraw and constrainToViewport option to draggable
+if(typeof(Draggable) != 'undefined'){
+ //allows the point to be modified with an onDraw callback
+ Draggable.prototype.draw = function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ if(this.options.ghosting) {
+ var r = Position.realOffset(this.element);
+ pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+ }
+
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(typeof this.options.snap == 'function') {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(this.options.snap instanceof Array) {
+ p = p.map( function(v, i) {return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ }
+ }
+ }
+
+ if(this.options.onDraw)
+ this.options.onDraw.bind(this)(p);
+ else{
+ var style = this.element.style;
+ if(this.options.constrainToViewport){
+ var viewport_dimensions = document.viewport.getDimensions();
+ var container_dimensions = this.element.getDimensions();
+ var margin_top = parseInt(this.element.getStyle('margin-top'));
+ var margin_left = parseInt(this.element.getStyle('margin-left'));
+ var boundary = [[
+ 0 - margin_left,
+ 0 - margin_top
+ ],[
+ (viewport_dimensions.width - container_dimensions.width) - margin_left,
+ (viewport_dimensions.height - container_dimensions.height) - margin_top
+ ]];
+ if((!this.options.constraint) || (this.options.constraint=='horizontal')){
+ if((p[0] >= boundary[0][0]) && (p[0] <= boundary[1][0]))
+ this.element.style.left = p[0] + "px";
+ else
+ this.element.style.left = ((p[0] < boundary[0][0]) ? boundary[0][0] : boundary[1][0]) + "px";
+ }
+ if((!this.options.constraint) || (this.options.constraint=='vertical')){
+ if((p[1] >= boundary[0][1] ) && (p[1] <= boundary[1][1]))
+ this.element.style.top = p[1] + "px";
+ else
+ this.element.style.top = ((p[1] <= boundary[0][1]) ? boundary[0][1] : boundary[1][1]) + "px";
+ }
+ }else{
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+ }
+ if(style.visibility=="hidden")
+ style.visibility = ""; // fix gecko rendering
+ }
+ };
+}
+
+if(typeof(Prototype) == "undefined")
+ throw "Control.Window requires Prototype to be loaded.";
+if(typeof(IframeShim) == "undefined")
+ throw "Control.Window requires IframeShim to be loaded.";
+if(typeof(Object.Event) == "undefined")
+ throw "Control.Window requires Object.Event to be loaded.";
+/*
+ known issues:
+ - when iframe is clicked is does not gain focus
+ - safari can't open multiple iframes properly
+ - constrainToViewport: body must have no margin or padding for this to work properly
+ - iframe will be mis positioned during fade in
+ - document.viewport does not account for scrollbars (this will eventually be fixed in the prototype core)
+ notes
+ - setting constrainToViewport only works when the page is not scrollable
+ - setting draggable: true will negate the effects of position: center
+*/
+Control.Window = Class.create({
+ initialize: function(container,options){
+ Control.Window.windows.push(this);
+
+ //attribute initialization
+ this.container = false;
+ this.isOpen = false;
+ this.href = false;
+ this.sourceContainer = false; //this is optionally the container that will open the window
+ this.ajaxRequest = false;
+ this.remoteContentLoaded = false; //this is set when the code to load the remote content is run, onRemoteContentLoaded is fired when the connection is closed
+ this.numberInSequence = Control.Window.windows.length + 1; //only useful for the effect scoping
+ this.indicator = false;
+ this.effects = {
+ fade: false,
+ appear: false
+ };
+ this.indicatorEffects = {
+ fade: false,
+ appear: false
+ };
+
+ //options
+ this.options = Object.extend({
+ //lifecycle
+ beforeOpen: Prototype.emptyFunction,
+ afterOpen: Prototype.emptyFunction,
+ beforeClose: Prototype.emptyFunction,
+ afterClose: Prototype.emptyFunction,
+ //dimensions and modes
+ height: null,
+ width: null,
+ className: false,
+ position: 'center', //'center', 'center_once', 'relative', [x,y], [function(){return x;},function(){return y;}]
+ offsetLeft: 0, //available only for anchors opening the window, or windows set to position: hover
+ offsetTop: 0, //""
+ iframe: false, //if the window has an href, this will display the href as an iframe instead of requesting the url as an an Ajax.Request
+ hover: false, //element object to hover over, or if "true" only available for windows with sourceContainer (an anchor or any element already on the page with an href attribute)
+ indicator: false, //element to show or hide when ajax requests, images and iframes are loading
+ closeOnClick: false, //does not work with hover,can be: true (click anywhere), 'container' (will refer to this.container), or element (a specific element)
+ iframeshim: true, //whether or not to position an iFrameShim underneath the window
+ //effects
+ fade: false,
+ fadeDuration: 0.75,
+ //draggable
+ draggable: false,
+ onDrag: Prototype.emptyFunction,
+ //resizable
+ resizable: false,
+ minHeight: false,
+ minWidth: false,
+ maxHeight: false,
+ maxWidth: false,
+ onResize: Prototype.emptyFunction,
+ //draggable and resizable
+ constrainToViewport: false,
+ //ajax
+ method: 'post',
+ parameters: {},
+ onComplete: Prototype.emptyFunction,
+ onSuccess: Prototype.emptyFunction,
+ onFailure: Prototype.emptyFunction,
+ onException: Prototype.emptyFunction,
+ //any element with an href (image,iframe,ajax) will call this after it is done loading
+ onRemoteContentLoaded: Prototype.emptyFunction,
+ insertRemoteContentAt: false //false will set this to this.container, can be string selector (first returned will be selected), or an Element that must be a child of this.container
+ },options || {});
+
+ //container setup
+ this.indicator = this.options.indicator ? $(this.options.indicator) : false;
+ if(container){
+ if(typeof(container) == "string" && container.match(Control.Window.uriRegex))
+ this.href = container;
+ else{
+ this.container = $(container);
+ //need to create the container now for tooltips (or hover: element with no container already on the page)
+ //second call made below will not create the container since the check is done inside createDefaultContainer()
+ this.createDefaultContainer(container);
+ //if an element with an href was passed in we use it to activate the window
+ if(this.container && ((this.container.readAttribute('href') && this.container.readAttribute('href') != '') || (this.options.hover && this.options.hover !== true))){
+ if(this.options.hover && this.options.hover !== true)
+ this.sourceContainer = $(this.options.hover);
+ else{
+ this.sourceContainer = this.container;
+ this.href = this.container.readAttribute('href');
+ var rel = this.href.match(/^#(.+)$/);
+ if(rel && rel[1]){
+ this.container = $(rel[1]);
+ this.href = false;
+ }else
+ this.container = false;
+ }
+ //hover or click handling
+ this.sourceContainerOpenHandler = function(event){
+ if (Event.isLeftClick(event)) {
+ // catches modifier keys
+ if (!(event.shiftKey || event.metaKey || event.ctrlKey || event.altKey)) {
+ this.open(event);
+ event.stop();
+ return false;
+ }
+ }
+ else if (Event.isMiddleClick(event)) {
+ // do nothing - let browser handle the click
+ }
+ else {
+ // must be triggered by hover or such
+ this.open(event);
+ event.stop();
+ return false;
+ }
+ }.bindAsEventListener(this);
+ this.sourceContainerCloseHandler = function(event){
+ this.close(event);
+ }.bindAsEventListener(this);
+ this.sourceContainerMouseMoveHandler = function(event){
+ this.position(event);
+ }.bindAsEventListener(this);
+ if(this.options.hover){
+ this.sourceContainer.observe('mouseenter',this.sourceContainerOpenHandler);
+ this.sourceContainer.observe('mouseleave',this.sourceContainerCloseHandler);
+ if(this.options.position == 'mouse')
+ this.sourceContainer.observe('mousemove',this.sourceContainerMouseMoveHandler);
+ }else
+ this.sourceContainer.observe('click',this.sourceContainerOpenHandler);
+ }
+ }
+ }
+ this.createDefaultContainer(container);
+ if(this.options.insertRemoteContentAt === false)
+ this.options.insertRemoteContentAt = this.container;
+ var styles = {
+ margin: 0,
+ position: 'absolute',
+ zIndex: Control.Window.initialZIndexForWindow()
+ };
+ if(this.options.width)
+ styles.width = $value(this.options.width) + 'px';
+ if(this.options.height)
+ styles.height = $value(this.options.height) + 'px';
+ this.container.setStyle(styles);
+ if(this.options.className)
+ this.container.addClassName(this.options.className);
+ this.positionHandler = this.position.bindAsEventListener(this);
+ this.outOfBoundsPositionHandler = this.ensureInBounds.bindAsEventListener(this);
+ this.bringToFrontHandler = this.bringToFront.bindAsEventListener(this);
+ this.container.observe('mousedown',this.bringToFrontHandler);
+ this.container.hide();
+ this.closeHandler = this.close.bindAsEventListener(this);
+ //iframeshim setup
+ if(this.options.iframeshim){
+ this.iFrameShim = new IframeShim();
+ this.iFrameShim.hide();
+ }
+ //resizable support
+ this.applyResizable();
+ //draggable support
+ this.applyDraggable();
+
+ //makes sure the window can't go out of bounds
+ Event.observe(window,'resize',this.outOfBoundsPositionHandler);
+
+ this.notify('afterInitialize');
+ },
+ open: function(event){
+ if(this.isOpen){
+ this.bringToFront();
+ return false;
+ }
+ if(this.notify('beforeOpen') === false)
+ return false;
+ //closeOnClick
+ if(this.options.closeOnClick){
+ if(this.options.closeOnClick === true)
+ this.closeOnClickContainer = $(document.body);
+ else if(this.options.closeOnClick == 'container')
+ this.closeOnClickContainer = this.container;
+ else if (this.options.closeOnClick == 'overlay'){
+ Control.Overlay.load();
+ this.closeOnClickContainer = Control.Overlay.container;
+ }else
+ this.closeOnClickContainer = $(this.options.closeOnClick);
+ this.closeOnClickContainer.observe('click',this.closeHandler);
+ }
+ if(this.href && !this.options.iframe && !this.remoteContentLoaded){
+ //link to image
+ this.remoteContentLoaded = true;
+ if(this.href.match(/\.(jpe?g|gif|png|tiff?)$/i)){
+ var img = new Element('img');
+ img.observe('load',function(img){
+ this.getRemoteContentInsertionTarget().insert(img);
+ this.position();
+ if(this.notify('onRemoteContentLoaded') !== false){
+ if(this.options.indicator)
+ this.hideIndicator();
+ this.finishOpen();
+ }
+ }.bind(this,img));
+ img.writeAttribute('src',this.href);
+ }else{
+ //if this is an ajax window it will only open if the request is successful
+ if(!this.ajaxRequest){
+ if(this.options.indicator)
+ this.showIndicator();
+ this.ajaxRequest = new Ajax.Request(this.href,{
+ method: this.options.method,
+ parameters: this.options.parameters,
+ onComplete: function(request){
+ this.notify('onComplete',request);
+ this.ajaxRequest = false;
+ }.bind(this),
+ onSuccess: function(request){
+ this.getRemoteContentInsertionTarget().insert(request.responseText);
+ this.notify('onSuccess',request);
+ if(this.notify('onRemoteContentLoaded') !== false){
+ if(this.options.indicator)
+ this.hideIndicator();
+ this.finishOpen();
+ }
+ }.bind(this),
+ onFailure: function(request){
+ this.notify('onFailure',request);
+ if(this.options.indicator)
+ this.hideIndicator();
+ }.bind(this),
+ onException: function(request,e){
+ this.notify('onException',request,e);
+ if(this.options.indicator)
+ this.hideIndicator();
+ }.bind(this)
+ });
+ }
+ }
+ return true;
+ }else if(this.options.iframe && !this.remoteContentLoaded){
+ //iframe
+ this.remoteContentLoaded = true;
+ if(this.options.indicator)
+ this.showIndicator();
+ this.getRemoteContentInsertionTarget().insert(Control.Window.iframeTemplate.evaluate({
+ href: this.href
+ }));
+ var iframe = this.container.down('iframe');
+ iframe.onload = function(){
+ this.notify('onRemoteContentLoaded');
+ if(this.options.indicator)
+ this.hideIndicator();
+ iframe.onload = null;
+ }.bind(this);
+ }
+ this.finishOpen(event);
+ return true
+ },
+ close: function(event){ //event may or may not be present
+ if(!this.isOpen || this.notify('beforeClose',event) === false)
+ return false;
+ if(this.options.closeOnClick)
+ this.closeOnClickContainer.stopObserving('click',this.closeHandler);
+ if(this.options.fade){
+ this.effects.fade = new Effect.Fade(this.container,{
+ queue: {
+ position: 'front',
+ scope: 'Control.Window' + this.numberInSequence
+ },
+ from: 1,
+ to: 0,
+ duration: this.options.fadeDuration / 2,
+ afterFinish: function(){
+ if(this.iFrameShim)
+ this.iFrameShim.hide();
+ this.isOpen = false;
+ this.notify('afterClose');
+ }.bind(this)
+ });
+ }else{
+ this.container.hide();
+ if(this.iFrameShim)
+ this.iFrameShim.hide();
+ }
+ if(this.ajaxRequest)
+ this.ajaxRequest.transport.abort();
+ if(!(this.options.draggable || this.options.resizable) && this.options.position == 'center')
+ Event.stopObserving(window,'resize',this.positionHandler);
+ if(!this.options.draggable && this.options.position == 'center')
+ Event.stopObserving(window,'scroll',this.positionHandler);
+ if(this.options.indicator)
+ this.hideIndicator();
+ if(!this.options.fade){
+ this.isOpen = false;
+ this.notify('afterClose');
+ }
+ return true;
+ },
+ position: function(event){
+ //this is up top for performance reasons
+ if(this.options.position == 'mouse'){
+ var xy = [Event.pointerX(event),Event.pointerY(event)];
+ this.container.setStyle({
+ top: xy[1] + $value(this.options.offsetTop) + 'px',
+ left: xy[0] + $value(this.options.offsetLeft) + 'px'
+ });
+ return;
+ }
+ var container_dimensions = this.container.getDimensions();
+ var viewport_dimensions = document.viewport.getDimensions();
+ Position.prepare();
+ var offset_left = (Position.deltaX + Math.floor((viewport_dimensions.width - container_dimensions.width) / 2));
+ var offset_top = (Position.deltaY + ((viewport_dimensions.height > container_dimensions.height) ? Math.floor((viewport_dimensions.height - container_dimensions.height) / 2) : 0));
+ if(this.options.position == 'center' || this.options.position == 'center_once'){
+ this.container.setStyle({
+ top: (container_dimensions.height <= viewport_dimensions.height) ? ((offset_top != null && offset_top > 0) ? offset_top : 0) + 'px' : 0,
+ left: (container_dimensions.width <= viewport_dimensions.width) ? ((offset_left != null && offset_left > 0) ? offset_left : 0) + 'px' : 0
+ });
+ }else if(this.options.position == 'relative'){
+ var xy = this.sourceContainer.cumulativeOffset();
+ var top = xy[1] + $value(this.options.offsetTop);
+ var left = xy[0] + $value(this.options.offsetLeft);
+ this.container.setStyle({
+ top: (container_dimensions.height <= viewport_dimensions.height) ? (this.options.constrainToViewport ? Math.max(0,Math.min(viewport_dimensions.height - (container_dimensions.height),top)) : top) + 'px' : 0,
+ left: (container_dimensions.width <= viewport_dimensions.width) ? (this.options.constrainToViewport ? Math.max(0,Math.min(viewport_dimensions.width - (container_dimensions.width),left)) : left) + 'px' : 0
+ });
+ }else if(this.options.position.length){
+ var top = $value(this.options.position[1]) + $value(this.options.offsetTop);
+ var left = $value(this.options.position[0]) + $value(this.options.offsetLeft);
+ this.container.setStyle({
+ top: (container_dimensions.height <= viewport_dimensions.height) ? (this.options.constrainToViewport ? Math.max(0,Math.min(viewport_dimensions.height - (container_dimensions.height),top)) : top) + 'px' : 0,
+ left: (container_dimensions.width <= viewport_dimensions.width) ? (this.options.constrainToViewport ? Math.max(0,Math.min(viewport_dimensions.width - (container_dimensions.width),left)) : left) + 'px' : 0
+ });
+ }
+ if(this.iFrameShim)
+ this.updateIFrameShimZIndex();
+ },
+ ensureInBounds: function(){
+ if(!this.isOpen)
+ return;
+ var viewport_dimensions = document.viewport.getDimensions();
+ var container_offset = this.container.cumulativeOffset();
+ var container_dimensions = this.container.getDimensions();
+ if(container_offset.left + container_dimensions.width > viewport_dimensions.width){
+ this.container.setStyle({
+ left: (Math.max(0,viewport_dimensions.width - container_dimensions.width)) + 'px'
+ });
+ }
+ if(container_offset.top + container_dimensions.height > viewport_dimensions.height){
+ this.container.setStyle({
+ top: (Math.max(0,viewport_dimensions.height - container_dimensions.height)) + 'px'
+ });
+ }
+ },
+ bringToFront: function(){
+ Control.Window.bringToFront(this);
+ this.notify('bringToFront');
+ },
+ destroy: function(){
+ this.container.stopObserving('mousedown',this.bringToFrontHandler);
+ if(this.draggable){
+ Draggables.removeObserver(this.container);
+ this.draggable.handle.stopObserving('mousedown',this.bringToFrontHandler);
+ this.draggable.destroy();
+ }
+ if(this.resizable){
+ Resizables.removeObserver(this.container);
+ this.resizable.handle.stopObserving('mousedown',this.bringToFrontHandler);
+ this.resizable.destroy();
+ }
+ if(this.container && !this.sourceContainer)
+ this.container.remove();
+ if(this.sourceContainer){
+ if(this.options.hover){
+ this.sourceContainer.stopObserving('mouseenter',this.sourceContainerOpenHandler);
+ this.sourceContainer.stopObserving('mouseleave',this.sourceContainerCloseHandler);
+ if(this.options.position == 'mouse')
+ this.sourceContainer.stopObserving('mousemove',this.sourceContainerMouseMoveHandler);
+ }else
+ this.sourceContainer.stopObserving('click',this.sourceContainerOpenHandler);
+ }
+ if(this.iFrameShim)
+ this.iFrameShim.destroy();
+ Event.stopObserving(window,'resize',this.outOfBoundsPositionHandler);
+ Control.Window.windows = Control.Window.windows.without(this);
+ this.notify('afterDestroy');
+ },
+ //private
+ applyResizable: function(){
+ if(this.options.resizable){
+ if(typeof(Resizable) == "undefined")
+ throw "Control.Window requires resizable.js to be loaded.";
+ var resizable_handle = null;
+ if(this.options.resizable === true){
+ resizable_handle = new Element('div',{
+ className: 'resizable_handle'
+ });
+ this.container.insert(resizable_handle);
+ }else
+ resizable_handle = $(this.options.resziable);
+ this.resizable = new Resizable(this.container,{
+ handle: resizable_handle,
+ minHeight: this.options.minHeight,
+ minWidth: this.options.minWidth,
+ maxHeight: this.options.constrainToViewport ? function(element){
+ //viewport height - top - total border height
+ return (document.viewport.getDimensions().height - parseInt(element.style.top || 0)) - (element.getHeight() - parseInt(element.style.height || 0));
+ } : this.options.maxHeight,
+ maxWidth: this.options.constrainToViewport ? function(element){
+ //viewport width - left - total border width
+ return (document.viewport.getDimensions().width - parseInt(element.style.left || 0)) - (element.getWidth() - parseInt(element.style.width || 0));
+ } : this.options.maxWidth
+ });
+ this.resizable.handle.observe('mousedown',this.bringToFrontHandler);
+ Resizables.addObserver(new Control.Window.LayoutUpdateObserver(this,function(){
+ if(this.iFrameShim)
+ this.updateIFrameShimZIndex();
+ this.notify('onResize');
+ }.bind(this)));
+ }
+ },
+ applyDraggable: function(){
+ if(this.options.draggable){
+ if(typeof(Draggables) == "undefined")
+ throw "Control.Window requires dragdrop.js to be loaded.";
+ var draggable_handle = null;
+ if(this.options.draggable === true){
+ draggable_handle = new Element('div',{
+ className: 'draggable_handle'
+ });
+ this.container.insert(draggable_handle);
+ }else
+ draggable_handle = $(this.options.draggable);
+ this.draggable = new Draggable(this.container,{
+ handle: draggable_handle,
+ constrainToViewport: this.options.constrainToViewport,
+ zindex: this.container.getStyle('z-index'),
+ starteffect: function(){
+ if(Prototype.Browser.IE){
+ this.old_onselectstart = document.onselectstart;
+ document.onselectstart = function(){
+ return false;
+ };
+ }
+ }.bind(this),
+ endeffect: function(){
+ document.onselectstart = this.old_onselectstart;
+ }.bind(this)
+ });
+ this.draggable.handle.observe('mousedown',this.bringToFrontHandler);
+ Draggables.addObserver(new Control.Window.LayoutUpdateObserver(this,function(){
+ if(this.iFrameShim)
+ this.updateIFrameShimZIndex();
+ this.notify('onDrag');
+ }.bind(this)));
+ }
+ },
+ createDefaultContainer: function(container){
+ if(!this.container){
+ //no container passed or found, create it
+ this.container = new Element('div',{
+ id: 'control_window_' + this.numberInSequence
+ });
+ $(document.body).insert(this.container);
+ if(typeof(container) == "string" && $(container) == null && !container.match(/^#(.+)$/) && !container.match(Control.Window.uriRegex))
+ this.container.update(container);
+ }
+ },
+ finishOpen: function(event){
+ this.bringToFront();
+ if(this.options.fade){
+ if(typeof(Effect) == "undefined")
+ throw "Control.Window requires effects.js to be loaded."
+ if(this.effects.fade)
+ this.effects.fade.cancel();
+ this.effects.appear = new Effect.Appear(this.container,{
+ queue: {
+ position: 'end',
+ scope: 'Control.Window.' + this.numberInSequence
+ },
+ from: 0,
+ to: 1,
+ duration: this.options.fadeDuration / 2,
+ afterFinish: function(){
+ if(this.iFrameShim)
+ this.updateIFrameShimZIndex();
+ this.isOpen = true;
+ this.notify('afterOpen');
+ }.bind(this)
+ });
+ }else
+ this.container.show();
+ this.position(event);
+ if(!(this.options.draggable || this.options.resizable) && this.options.position == 'center')
+ Event.observe(window,'resize',this.positionHandler,false);
+ if(!this.options.draggable && this.options.position == 'center')
+ Event.observe(window,'scroll',this.positionHandler,false);
+ if(!this.options.fade){
+ this.isOpen = true;
+ this.notify('afterOpen');
+ }
+ return true;
+ },
+ showIndicator: function(){
+ this.showIndicatorTimeout = window.setTimeout(function(){
+ if(this.options.fade){
+ this.indicatorEffects.appear = new Effect.Appear(this.indicator,{
+ queue: {
+ position: 'front',
+ scope: 'Control.Window.indicator.' + this.numberInSequence
+ },
+ from: 0,
+ to: 1,
+ duration: this.options.fadeDuration / 2
+ });
+ }else
+ this.indicator.show();
+ }.bind(this),Control.Window.indicatorTimeout);
+ },
+ hideIndicator: function(){
+ if(this.showIndicatorTimeout)
+ window.clearTimeout(this.showIndicatorTimeout);
+ this.indicator.hide();
+ },
+ getRemoteContentInsertionTarget: function(){
+ return typeof(this.options.insertRemoteContentAt) == "string" ? this.container.down(this.options.insertRemoteContentAt) : $(this.options.insertRemoteContentAt);
+ },
+ updateIFrameShimZIndex: function(){
+ if(this.iFrameShim)
+ this.iFrameShim.positionUnder(this.container);
+ }
+});
+//class methods
+Object.extend(Control.Window,{
+ windows: [],
+ baseZIndex: 9999,
+ indicatorTimeout: 250,
+ iframeTemplate: new Template(''),
+ uriRegex: /^(\/|\#|https?\:\/\/|[\w]+\/)/,
+ bringToFront: function(w){
+ Control.Window.windows = Control.Window.windows.without(w);
+ Control.Window.windows.push(w);
+ Control.Window.windows.each(function(w,i){
+ var z_index = Control.Window.baseZIndex + i;
+ w.container.setStyle({
+ zIndex: z_index
+ });
+ if(w.isOpen){
+ if(w.iFrameShim)
+ w.updateIFrameShimZIndex();
+ }
+ if(w.options.draggable)
+ w.draggable.options.zindex = z_index;
+ });
+ },
+ open: function(container,options){
+ var w = new Control.Window(container,options);
+ w.open();
+ return w;
+ },
+ //protected
+ initialZIndexForWindow: function(w){
+ return Control.Window.baseZIndex + (Control.Window.windows.length - 1);
+ }
+});
+Object.Event.extend(Control.Window);
+
+//this is the observer for both Resizables and Draggables
+Control.Window.LayoutUpdateObserver = Class.create({
+ initialize: function(w,observer){
+ this.w = w;
+ this.element = $(w.container);
+ this.observer = observer;
+ },
+ onStart: Prototype.emptyFunction,
+ onEnd: function(event_name,instance){
+ if(instance.element == this.element && this.iFrameShim)
+ this.w.updateIFrameShimZIndex();
+ },
+ onResize: function(event_name,instance){
+ if(instance.element == this.element)
+ this.observer(this.element);
+ },
+ onDrag: function(event_name,instance){
+ if(instance.element == this.element)
+ this.observer(this.element);
+ }
+});
+
+//overlay for Control.Modal
+Control.Overlay = {
+ id: 'control_overlay',
+ loaded: false,
+ container: false,
+ lastOpacity: 0,
+ styles: {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: 9998
+ },
+ ieStyles: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ zIndex: 9998
+ },
+ effects: {
+ fade: false,
+ appear: false
+ },
+ load: function(){
+ if(Control.Overlay.loaded)
+ return false;
+ Control.Overlay.loaded = true;
+ Control.Overlay.container = new Element('div',{
+ id: Control.Overlay.id
+ });
+ $(document.body).insert(Control.Overlay.container);
+ if(Prototype.Browser.IE){
+ Control.Overlay.container.setStyle(Control.Overlay.ieStyles);
+ Event.observe(window,'scroll',Control.Overlay.positionOverlay);
+ Event.observe(window,'resize',Control.Overlay.positionOverlay);
+ Control.Overlay.observe('beforeShow',Control.Overlay.positionOverlay);
+ }else
+ Control.Overlay.container.setStyle(Control.Overlay.styles);
+ Control.Overlay.iFrameShim = new IframeShim();
+ Control.Overlay.iFrameShim.hide();
+ Event.observe(window,'resize',Control.Overlay.positionIFrameShim);
+ Control.Overlay.container.hide();
+ return true;
+ },
+ unload: function(){
+ if(!Control.Overlay.loaded)
+ return false;
+ Event.stopObserving(window,'resize',Control.Overlay.positionOverlay);
+ Control.Overlay.stopObserving('beforeShow',Control.Overlay.positionOverlay);
+ Event.stopObserving(window,'resize',Control.Overlay.positionIFrameShim);
+ Control.Overlay.iFrameShim.destroy();
+ Control.Overlay.container.remove();
+ Control.Overlay.loaded = false;
+ return true;
+ },
+ show: function(opacity,fade){
+ if(Control.Overlay.notify('beforeShow') === false)
+ return false;
+ Control.Overlay.lastOpacity = opacity;
+ Control.Overlay.positionIFrameShim();
+ Control.Overlay.iFrameShim.show();
+ if(fade){
+ if(typeof(Effect) == "undefined")
+ throw "Control.Window requires effects.js to be loaded."
+ if(Control.Overlay.effects.fade)
+ Control.Overlay.effects.fade.cancel();
+ Control.Overlay.effects.appear = new Effect.Appear(Control.Overlay.container,{
+ queue: {
+ position: 'end',
+ scope: 'Control.Overlay'
+ },
+ afterFinish: function(){
+ Control.Overlay.notify('afterShow');
+ },
+ from: 0,
+ to: Control.Overlay.lastOpacity,
+ duration: (fade === true ? 0.75 : fade) / 2
+ });
+ }else{
+ Control.Overlay.container.setStyle({
+ opacity: opacity || 1
+ });
+ Control.Overlay.container.show();
+ Control.Overlay.notify('afterShow');
+ }
+ return true;
+ },
+ hide: function(fade){
+ if(Control.Overlay.notify('beforeHide') === false)
+ return false;
+ if(Control.Overlay.effects.appear)
+ Control.Overlay.effects.appear.cancel();
+ Control.Overlay.iFrameShim.hide();
+ if(fade){
+ Control.Overlay.effects.fade = new Effect.Fade(Control.Overlay.container,{
+ queue: {
+ position: 'front',
+ scope: 'Control.Overlay'
+ },
+ afterFinish: function(){
+ Control.Overlay.notify('afterHide');
+ },
+ from: Control.Overlay.lastOpacity,
+ to: 0,
+ duration: (fade === true ? 0.75 : fade) / 2
+ });
+ }else{
+ Control.Overlay.container.hide();
+ Control.Overlay.notify('afterHide');
+ }
+ return true;
+ },
+ positionIFrameShim: function(){
+ if(Control.Overlay.container.visible())
+ Control.Overlay.iFrameShim.positionUnder(Control.Overlay.container);
+ },
+ //IE only
+ positionOverlay: function(){
+ Control.Overlay.container.setStyle({
+ width: document.body.clientWidth + 'px',
+ height: document.body.clientHeight + 'px'
+ });
+ }
+};
+Object.Event.extend(Control.Overlay);
+
+Control.ToolTip = Class.create(Control.Window,{
+ initialize: function($super,container,tooltip,options){
+ $super(tooltip,Object.extend(Object.extend(Object.clone(Control.ToolTip.defaultOptions),options || {}),{
+ position: 'mouse',
+ hover: container
+ }));
+ }
+});
+Object.extend(Control.ToolTip,{
+ defaultOptions: {
+ offsetLeft: 10
+ }
+});
+
+Control.Modal = Class.create(Control.Window,{
+ initialize: function($super,container,options){
+ Control.Modal.InstanceMethods.beforeInitialize.bind(this)();
+ $super(container,Object.extend(Object.clone(Control.Modal.defaultOptions),options || {}));
+ },
+ closeWithoutOverlay: function(){
+ this.keepOverlay = true;
+ this.close();
+ }
+});
+Object.extend(Control.Modal,{
+ defaultOptions: {
+ overlayOpacity: 0.5,
+ closeOnClick: 'overlay'
+ },
+ current: false,
+ open: function(container,options){
+ var modal = new Control.Modal(container,options);
+ modal.open();
+ return modal;
+ },
+ close: function(){
+ if(Control.Modal.current)
+ Control.Modal.current.close();
+ },
+ InstanceMethods: {
+ beforeInitialize: function(){
+ Control.Overlay.load();
+ this.observe('beforeOpen',Control.Modal.Observers.beforeOpen.bind(this));
+ this.observe('afterOpen',Control.Modal.Observers.afterOpen.bind(this));
+ this.observe('afterClose',Control.Modal.Observers.afterClose.bind(this));
+ }
+ },
+ Observers: {
+ beforeOpen: function(){
+ Control.Window.windows.without(this).each(function(w){
+ if(w.closeWithoutOverlay && w.isOpen){
+ w.closeWithoutOverlay();
+ }else{
+ w.close();
+ }
+ });
+ if(!Control.Overlay.overlayFinishedOpening){
+ Control.Overlay.observeOnce('afterShow',function(){
+ Control.Overlay.overlayFinishedOpening = true;
+ this.open();
+ }.bind(this));
+ Control.Overlay.show(this.options.overlayOpacity,this.options.fade ? this.options.fadeDuration : false);
+ throw $break;
+ }
+ },
+ afterOpen: function(){
+ Control.Overlay.show(this.options.overlayOpacity);
+ Control.Overlay.overlayFinishedOpening = true;
+ Control.Modal.current = this;
+ },
+ afterClose: function(){
+ if(!this.keepOverlay){
+ Control.Overlay.hide(this.options.fade ? this.options.fadeDuration : false);
+ Control.Overlay.overlayFinishedOpening = false;
+ }
+ this.keepOverlay = false;
+ Control.Modal.current = false;
+ }
+ }
+});
+
+Control.LightBox = Class.create(Control.Window,{
+ initialize: function($super,container,options){
+ this.allImagesLoaded = false;
+ if(options.modal){
+ var options = Object.extend(Object.clone(Control.LightBox.defaultOptions),options || {});
+ options = Object.extend(Object.clone(Control.Modal.defaultOptions),options);
+ options = Control.Modal.InstanceMethods.beforeInitialize.bind(this)(options);
+ $super(container,options);
+ }else
+ $super(container,Object.extend(Object.clone(Control.LightBox.defaultOptions),options || {}));
+ this.hasRemoteContent = this.href && !this.options.iframe;
+ if(this.hasRemoteContent)
+ this.observe('onRemoteContentLoaded',Control.LightBox.Observers.onRemoteContentLoaded.bind(this));
+ else
+ this.applyImageObservers();
+ this.observe('beforeOpen',Control.LightBox.Observers.beforeOpen.bind(this));
+ },
+ applyImageObservers:function(){
+ var images = this.getImages();
+ this.numberImagesToLoad = images.length;
+ this.numberofImagesLoaded = 0;
+ images.each(function(image){
+ image.observe('load',function(image){
+ ++this.numberofImagesLoaded;
+ if(this.numberImagesToLoad == this.numberofImagesLoaded){
+ this.allImagesLoaded = true;
+ this.onAllImagesLoaded();
+ }
+ }.bind(this,image));
+ image.hide();
+ }.bind(this));
+ },
+ onAllImagesLoaded: function(){
+ this.getImages().each(function(image){
+ this.showImage(image);
+ }.bind(this));
+ if(this.hasRemoteContent){
+ if(this.options.indicator)
+ this.hideIndicator();
+ this.finishOpen();
+ }else
+ this.open();
+ },
+ getImages: function(){
+ return this.container.select(Control.LightBox.imageSelector);
+ },
+ showImage: function(image){
+ image.show();
+ }
+});
+Object.extend(Control.LightBox,{
+ imageSelector: 'img',
+ defaultOptions: {},
+ Observers: {
+ beforeOpen: function(){
+ if(!this.hasRemoteContent && !this.allImagesLoaded)
+ throw $break;
+ },
+ onRemoteContentLoaded: function(){
+ this.applyImageObservers();
+ if(!this.allImagesLoaded)
+ throw $break;
+ }
+ }
+});
diff --git a/vendored-plugins/openproject-costs/.gitignore b/vendored-plugins/openproject-costs/.gitignore
new file mode 100644
index 0000000000..944c283430
--- /dev/null
+++ b/vendored-plugins/openproject-costs/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.idea/
diff --git a/vendored-plugins/openproject-costs/.hound.yml b/vendored-plugins/openproject-costs/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-costs/.rubocop.yml b/vendored-plugins/openproject-costs/.rubocop.yml
new file mode 100644
index 0000000000..2813c933b9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/.rubocop.yml
@@ -0,0 +1,274 @@
+AllCops:
+ Exclude:
+ - "*.gemspec"
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+BlockDelimiters:
+ Enabled: true
+ EnforcedStyle: semantic
+ IgnoredMethods:
+ - default_scope
+ - lambda
+ - proc
+ - it
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
diff --git a/vendored-plugins/openproject-costs/README.md b/vendored-plugins/openproject-costs/README.md
new file mode 100644
index 0000000000..b0daf31570
--- /dev/null
+++ b/vendored-plugins/openproject-costs/README.md
@@ -0,0 +1,72 @@
+OpenProject Costs Plugin
+===========================
+
+This Plugin adds features for planning and tracking costs of projects. Budgets can be created containing the planned unit costs and labor costs. The actual costs can be assigned to the different work packages and planned and actual costs can be compared.
+
+A more detailed description can be found on [OpenProject.org](https://community.openproject.org/projects/openproject/wiki/Time_and_Cost).
+
+
+Requirements
+------------
+
+The OpenProject Costs plug-in requires the [OpenProject Core](https://github.com/opf/openproject/) in the same version.
+
+
+Installation
+------------
+
+For OpenProject Costs itself you need to add the following line to the `Gemfile.plugins` of OpenProject (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-costs", git: "https://github.com/finnlabs/openproject-costs.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-costs", git: "https://github.com/finnlabs/openproject-costs.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this leaves plugin data in the database. Currently, we do not support full uninstall of the plugin.
+
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/costs-plugin
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/openproject-costs`
+
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+
+Licence
+-------
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_objects.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_objects.js
new file mode 100644
index 0000000000..1a1fbb8d50
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_objects.js
@@ -0,0 +1,48 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+function deleteBudgetItem(id, field) {
+ $(id + '_' + field).value = 0;
+ $(id).hide();
+}
+
+function deleteMaterialBudgetItem(id) { deleteBudgetItem(id, 'units') }
+function deleteLaborBudgetItem(id) { deleteBudgetItem(id, 'hours') }
+
+function confirmChangeType(text, select, originalValue) {
+ if (originalValue == "") return true;
+ var ret = confirm(text);
+ if (!ret) select.setValue(originalValue);
+ return ret;
+}
+
+jQuery(function($) {
+ $(window).load(function () {
+ $('.action_menu_specific > .icon-edit').click(function () {
+ var scrollToId = "#update",
+ focusId = "#cost_object_description";
+ $(scrollToId).show();
+ $('html, body').animate({
+ scrollTop: $(scrollToId).offset().top
+ }, 500);
+ $(focusId).focus();
+ return false;
+ });
+ });
+});
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_types.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_types.js
new file mode 100644
index 0000000000..189d2f0348
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/cost_types.js
@@ -0,0 +1,32 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+function submitForm(event, el) {
+ submitFormWithConfirmation(event, el, true);
+}
+
+function submitFormWithConfirmation(event, el, withConfirmation) {
+ event.preventDefault();
+
+ if (!withConfirmation || confirm(I18n.t("js.text_are_you_sure"))) {
+ jQuery(el).parent().submit();
+ }
+
+ return false;
+}
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/costs.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/costs.js
new file mode 100644
index 0000000000..1e05ec555a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/costs.js
@@ -0,0 +1,24 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+//= require costs/cost_objects
+//= require costs/cost_types
+//= require costs/subform
+//= require costs/rates
+//= require costs/editinplace
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/editinplace.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/editinplace.js
new file mode 100644
index 0000000000..ca66358d60
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/editinplace.js
@@ -0,0 +1,72 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+function initialize_editinplace(cancelButtonAttributes) {
+ _cancelButtonAttributes = cancelButtonAttributes;
+}
+
+function getCurrencyValue(str) {
+ var result = str.match(/^\s*(([0-9]+[.,])+[0-9]+) (.+)\s*/);
+ return result ? new Array(result[1], result[3]) : new Array(str, "");
+}
+
+function makeEditable(id, name){
+ var obj = $(id);
+ obj.addClassName("inline_editable");
+ Event.observe(id, 'click', function(){edit_and_focus(obj, name)});
+
+}
+
+function edit_and_focus(obj, name) {
+ edit(obj, name);
+
+ Form.Element.focus(obj.id+'_edit');
+ Form.Element.select(obj.id+'_edit');
+}
+
+function edit(obj, name, obj_value) {
+ Element.hide(obj);
+
+ var obj_value = typeof(obj_value) != 'undefined' ? obj_value : obj.innerHTML;
+ var parsed = getCurrencyValue(obj_value);
+ var value = parsed[0];
+ var currency = parsed[1];
+
+ var form_start = '';
+
+ new Insertion.After(obj, form_start + button + span + affix + form_end);
+
+ Event.observe(obj.id+'_cancel', 'click', function(){cleanUp(obj)});
+}
+
+function cleanUp(obj){
+ Element.remove(obj.id+'_section');
+ Element.show(obj);
+}
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/rates.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/rates.js
new file mode 100644
index 0000000000..1cc3b63732
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/rates.js
@@ -0,0 +1,41 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+function addRate(date_field){
+ RatesForm.add_on_top();
+
+ var newRateRow = $(RatesForm.parentElement).down("tr");
+ var validFromField = newRateRow.down('input.date')
+ validFromField.value = jQuery.datepicker.formatDate('yy-mm-dd', new Date());
+ newRateRow.down('td.currency').down('input').select();
+}
+
+function deleteRow(image){
+ var row = image.up("tr")
+ var parent=row.up();
+ row.remove();
+ recalculate_even_odd(parent);
+}
+
+jQuery(function(jQuery){
+ jQuery(document).on("click", "body.action-edit a.delete-rate", function(){
+ deleteRow(this);
+ return false;
+ });
+});
diff --git a/vendored-plugins/openproject-costs/app/assets/javascripts/costs/subform.js b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/subform.js
new file mode 100644
index 0000000000..0a0ba3d565
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/javascripts/costs/subform.js
@@ -0,0 +1,61 @@
+//-- copyright
+// OpenProject Costs Plugin
+//
+// Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+var Subform = Class.create({
+ lineIndex: 1,
+ parentElement: "",
+ initialize: function(rawHTML, lineIndex, parentElement) {
+ this.rawHTML = rawHTML;
+ this.lineIndex = lineIndex;
+ this.parentElement = parentElement;
+ },
+
+ parsedHTML: function() {
+ return this.rawHTML.replace(/INDEX/g, this.lineIndex++);
+ },
+
+ add: function() {
+ var e = $(this.parentElement);
+ Element.insert(e, { bottom: this.parsedHTML()});
+ recalculate_even_odd(e);
+ },
+
+ add_after: function(e) {
+ Element.insert(e, { after: this.parsedHTML()});
+ recalculate_even_odd($(this.parentElement));
+ },
+
+ add_on_top: function() {
+ var e = $(this.parentElement);
+ Element.insert(e, { top: this.parsedHTML()});
+ recalculate_even_odd(e);
+ }
+});
+
+function recalculate_even_odd(element) {
+ $A(element.childElements()).inject(
+ 0,
+ function(acc, e)
+ {
+ e.removeClassName("even");
+ e.removeClassName("odd");
+ e.addClassName( (acc%2==0) ? "odd" : "even"); return ++acc;
+ }
+ )
+}
diff --git a/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/_summarized_cost_entries.sass b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/_summarized_cost_entries.sass
new file mode 100644
index 0000000000..2ac0367dcc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/_summarized_cost_entries.sass
@@ -0,0 +1,23 @@
+/*-- copyright
+ * OpenProject Costs Plugin
+ *
+ * Copyright (C) 2009 - 2015 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+.costs.costTypes
+ .separator
+ margin-left: -0.2em
diff --git a/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs.css.sass b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs.css.sass
new file mode 100644
index 0000000000..140338e657
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs.css.sass
@@ -0,0 +1,23 @@
+/*-- copyright
+ * OpenProject Costs Plugin
+ *
+ * Copyright (C) 2009 - 2015 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+@import costs/costs_legacy
+@import costs/summarized_cost_entries
+@import costs/settings
diff --git a/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs_legacy.css b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs_legacy.css
new file mode 100644
index 0000000000..6604512447
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/costs_legacy.css
@@ -0,0 +1,123 @@
+/*-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++*/
+
+.wide-icon {
+ background-position: 0% 40%;
+ background-repeat: no-repeat;
+ padding-left: 40px;
+ padding-top: 2px;
+ padding-bottom: 3px;
+
+}
+
+.single-wide-icon {
+ background-position: 20px 40%;
+}
+
+.filter_values select {
+ width: 200px;
+}
+
+table.material_budget_items,
+table.labor_budget_items,
+table.cost_entries {
+ width: 100%;
+}
+
+table.cost_types,
+table.rates {
+ text-align: center;
+}
+
+.material_budget_items tr,
+.labor_budget_items tr,
+.cost_entries tr {
+ text-align: center;
+ vertical-align: top;
+}
+
+tr.cost_entry {
+ text-align: center;
+ white-space: nowrap;
+}
+
+th.currency,
+td.currency,
+.currency {
+ text-align: right;
+ white-space: nowrap;
+}
+
+td.subject,
+td.comment {
+ text-align: left;
+}
+
+table.labor_budget_items, td.hours {
+ font-weight: normal;
+ padding-right: .5em;
+ text-align: right;
+}
+
+td.units {
+ font-weight: normal;
+ padding-right: .5em;
+ text-align: left;
+}
+
+
+td.comment input {
+ width: 99%; /* not 100% for IE6 */
+}
+
+.tabular div.p {
+ clear: left;
+ height: 1%;
+ margin: 0;
+ padding: 5px 0 8px 180px;
+}
+
+.inline_editable {
+ cursor: pointer;
+}
+
+.budget-table--fieldset{
+ margin: 8px 2px;
+}
+
+.cost_entry td{
+ vertical-align: top;
+ padding: 6px 6px;
+}
+
+.budget-table--fields{
+ text-align: left;
+}
+
+#budget-table--submit-button{
+ margin: 1rem 1rem 1rem 0.1rem;
+}
+
+table.progress td.exceeded { background: #E1B9B9 none repeat scroll 0%; }
+
+fieldset#group-by table { border-collapse: collapse; }
+fieldset#group-by table td { padding: 0; vertical-align: middle; }
+fieldset#group-by tr.filter { height: 2em; }
+fieldset#group-by td.add-group-by { text-align: right; vertical-align: top; }
diff --git a/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/settings.sass b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/settings.sass
new file mode 100644
index 0000000000..486b9d11dd
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/assets/stylesheets/costs/settings.sass
@@ -0,0 +1,23 @@
+/*-- copyright
+ * OpenProject Costs Plugin
+ *
+ * Copyright (C) 2009 - 2015 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+table.list.members
+ .form--text-field.-tiny
+ min-width: 60px
diff --git a/vendored-plugins/openproject-costs/app/controllers/cost_objects_controller.rb b/vendored-plugins/openproject-costs/app/controllers/cost_objects_controller.rb
new file mode 100644
index 0000000000..81aaea2b60
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/controllers/cost_objects_controller.rb
@@ -0,0 +1,266 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostObjectsController < ApplicationController
+ before_filter :find_cost_object, only: [:show, :edit, :update, :copy]
+ before_filter :find_cost_objects, only: :destroy
+ before_filter :find_project, only: [
+ :new, :create,
+ :update_material_budget_item, :update_labor_budget_item
+ ]
+ before_filter :find_optional_project, only: :index
+
+ before_filter :authorize_global, only: :index
+ before_filter :authorize, except: [
+ # unrestricted actions
+ :index,
+ :update_material_budget_item, :update_labor_budget_item
+ ]
+
+ helper :sort
+ include SortHelper
+ helper :projects
+ include ProjectsHelper
+ helper :attachments
+ include AttachmentsHelper
+ helper :costlog
+ include CostlogHelper
+ helper :cost_objects
+ include CostObjectsHelper
+ include WorkPackage::PdfExporter
+ include PaginationHelper
+
+ def index
+ respond_to do |format|
+ format.html do end
+ format.csv { limit = Setting.work_packages_export_limit.to_i }
+ end
+
+ sort_columns = { 'id' => "#{CostObject.table_name}.id",
+ 'subject' => "#{CostObject.table_name}.subject",
+ 'fixed_date' => "#{CostObject.table_name}.fixed_date"
+ }
+
+ sort_init 'id', 'desc'
+ sort_update sort_columns
+
+ allowed_condition = Project.allowed_to(User.current,
+ :view_cost_objects,
+ project: @project)
+
+ @cost_objects = CostObject.order(sort_clause)
+ .includes(:project, :author)
+ .merge(allowed_condition)
+ .page(page_param)
+ .per_page(per_page_param)
+
+ respond_to do |format|
+ format.html do render action: 'index', layout: !request.xhr? end
+ format.csv { send_data(cost_objects_to_csv(@cost_objects), type: 'text/csv; header=present', filename: 'export.csv') }
+ end
+ end
+
+ def show
+ @edit_allowed = User.current.allowed_to?(:edit_cost_objects, @project)
+ respond_to do |format|
+ format.html { render action: 'show', layout: !request.xhr? }
+ end
+ end
+
+ def new
+ # FIXME: I forcibly create a VariableCostObject for now. Following Ticket #5360
+ @cost_object ||= VariableCostObject.new
+ @cost_object.project_id = @project.id
+ @cost_object.fixed_date ||= Date.today
+
+ render layout: !request.xhr?
+ end
+
+ def copy
+ source = CostObject.find(params[:id].to_i)
+ if source
+ @cost_object = create_cost_object(source.kind)
+ @cost_object.copy_from(source)
+ end
+
+ # FIXME: I forcibly create a VariableCostObject for now. Following Ticket #5360
+ @cost_object ||= VariableCostObject.new
+ @cost_object.fixed_date ||= Date.today
+
+ render action: :new, layout: !request.xhr?
+ end
+
+ def create
+ if params[:cost_object]
+ @cost_object = create_cost_object(params[:cost_object].delete(:kind))
+ end
+
+ # FIXME: I forcibly create a VariableCostObject for now. Following Ticket #5360
+ @cost_object ||= VariableCostObject.new
+
+ @cost_object.project_id = @project.id
+
+ # fixed_date must be set before material_budget_items and labor_budget_items
+ if params[:cost_object] && params[:cost_object][:fixed_date]
+ @cost_object.fixed_date = params[:cost_object].delete(:fixed_date)
+ else
+ @cost_object.fixed_date = Date.today
+ end
+
+ @cost_object.attributes = permitted_params.cost_object
+
+ if @cost_object.save
+ Attachment.attach_files(@cost_object, params[:attachments])
+ render_attachment_warning_if_needed(@cost_object)
+
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to(params[:continue] ? { action: 'new' } :
+ { action: 'show', id: @cost_object })
+ return
+ else
+ render action: 'new', layout: !request.xhr?
+ end
+ end
+
+ def edit
+ # TODO: This method used to be responsible for both edit and update
+ # Please remove code where necessary
+ # check whether this method is needed at all
+ @cost_object.attributes = permitted_params.cost_object if params[:cost_object]
+ end
+
+ def update
+ # TODO: This was simply copied over from edit in order to have
+ # something as a starting point for separating the two
+ # Please go ahead and start removing code where necessary
+
+ # TODO: use better way to prevent mass assignment errors
+ params[:cost_object].delete(:kind)
+ @cost_object.attributes = permitted_params.cost_object if params[:cost_object]
+
+ if @cost_object.save
+ Attachment.attach_files(@cost_object, params[:attachments])
+ render_attachment_warning_if_needed(@cost_object)
+
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to(params[:back_to] || { action: 'show', id: @cost_object })
+ else
+ render action: 'edit'
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ end
+
+ def destroy
+ @cost_objects.each(&:destroy)
+ flash[:notice] = l(:notice_successful_delete)
+ redirect_to action: 'index', project_id: @project
+ end
+
+ def update_material_budget_item
+ element_id = params[:element_id] if params.has_key? :element_id
+
+ cost_type = CostType.find(params[:cost_type_id]) if params.has_key? :cost_type_id
+
+ units = BigDecimal.new(Rate.clean_currency(params[:units]))
+ costs = (units * cost_type.rate_at(params[:fixed_date]).rate rescue 0.0)
+
+ if request.xhr?
+ render :update do |page|
+ if User.current.allowed_to? :view_cost_rates, @project
+ page.replace_html "#{element_id}_costs", number_to_currency(costs)
+ end
+ page.replace_html "#{element_id}_unit_name", h(units == 1.0 ? cost_type.unit : cost_type.unit_plural)
+ end
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def update_labor_budget_item
+ element_id = params[:element_id] if params.has_key? :element_id
+
+ user = User.find(params[:user_id])
+
+ hours = params[:hours].to_hours
+ costs = hours * user.rate_at(params[:fixed_date], @project).rate rescue 0.0
+
+ if request.xhr?
+ render :update do |page|
+ if User.current.allowed_to?(:view_hourly_rates, @project, for: user)
+ page.replace_html "#{element_id}_costs", number_to_currency(costs)
+ end
+ end
+ end
+ rescue ActiveRecord::RecordNotFound
+ render :update do |page|
+ page.replace_html "#{element_id}_costs", number_to_currency(0.0)
+ end
+ end
+
+ private
+
+ def create_cost_object(kind)
+ case kind
+ when FixedCostObject.name
+ FixedCostObject.new
+ when VariableCostObject.name
+ VariableCostObject.new
+ else
+ CostObject.new
+ end
+ end
+
+ def find_cost_object
+ # This function comes directly from issues_controller.rb (Redmine 0.8.4)
+ @cost_object = CostObject.includes(:project, :author).find_by(id: params[:id])
+ @project = @cost_object.project if @cost_object
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_cost_objects
+ # This function comes directly from issues_controller.rb (Redmine 0.8.4)
+
+ @cost_objects = CostObject.where(id: params[:id] || params[:ids])
+ raise ActiveRecord::RecordNotFound if @cost_objects.empty?
+ projects = @cost_objects.map(&:project).compact.uniq
+ if projects.size == 1
+ @project = projects.first
+ else
+ # TODO: let users bulk edit/move/destroy cost_objects from different projects
+ render_error 'Can not bulk edit/move/destroy cost objects from different projects' and return false
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ @project = Project.find(params[:project_id]) unless params[:project_id].blank?
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/controllers/cost_types_controller.rb b/vendored-plugins/openproject-costs/app/controllers/cost_types_controller.rb
new file mode 100644
index 0000000000..a7cefdfeb5
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/controllers/cost_types_controller.rb
@@ -0,0 +1,145 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostTypesController < ApplicationController
+ # Allow only admins here
+ before_filter :require_admin
+ before_filter :find_cost_type, only: [:edit, :update, :set_rate, :destroy, :restore]
+ layout 'admin'
+
+ helper :sort
+ include SortHelper
+ helper :cost_types
+ include CostTypesHelper
+
+ def index
+ sort_init 'name', 'asc'
+ sort_columns = { 'name' => "#{CostType.table_name}.name",
+ 'unit' => "#{CostType.table_name}.unit",
+ 'unit_plural' => "#{CostType.table_name}.unit_plural" }
+ sort_update sort_columns
+
+ @cost_types = CostType.order(sort_clause)
+
+ unless params[:clear_filter]
+ @fixed_date = Date.parse(params[:fixed_date]) rescue Date.today
+ @include_deleted = params[:include_deleted]
+ else
+ @fixed_date = Date.today
+ @include_deleted = nil
+ end
+
+ render action: 'index', layout: !request.xhr?
+ end
+
+ def edit
+ render action: 'edit', layout: !request.xhr?
+ end
+
+ def update
+ @cost_type.attributes = permitted_params.cost_type
+
+ if @cost_type.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_back_or_default(action: 'index')
+ else
+ render action: 'edit', layout: !request.xhr?
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ end
+
+ def new
+ @cost_type = CostType.new
+
+ @cost_type.rates.build(valid_from: Date.today) if @cost_type.rates.empty?
+
+ render action: 'edit', layout: !request.xhr?
+ end
+
+ def create
+ @cost_type = CostType.new(permitted_params.cost_type)
+
+ if @cost_type.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_back_or_default(action: 'index')
+ else
+ @cost_type.rates.build(valid_from: Date.today) if @cost_type.rates.empty?
+ render action: 'edit', layout: !request.xhr?
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ end
+
+ def destroy
+ @cost_type.deleted_at = DateTime.now
+ @cost_type.default = false
+
+ if @cost_type.save
+ flash[:notice] = l(:notice_successful_lock)
+
+ redirect_back_or_default(action: 'index')
+ end
+ end
+
+ def restore
+ @cost_type.deleted_at = nil
+ @cost_type.default = false
+
+ if @cost_type.save
+ flash[:notice] = l(:notice_successful_restore)
+
+ redirect_back_or_default(action: 'index')
+ end
+ end
+
+ def set_rate
+ today = Date.today
+
+ rate = @cost_type.rate_at(today)
+ rate ||= CostRate.new.tap do |cr|
+ cr.cost_type = @cost_type
+ cr.valid_from = today
+ end
+
+ rate.rate = clean_currency(params[:rate])
+ if rate.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to action: 'index'
+ else
+ # FIXME: Do some real error handling here
+ flash[:error] = l(:notice_something_wrong)
+ redirect_to action: 'index'
+ end
+ end
+
+ private
+
+ def find_cost_type
+ @cost_type = CostType.find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def default_breadcrumb
+ CostType.model_name.human(count: 2)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/controllers/costlog_controller.rb b/vendored-plugins/openproject-costs/app/controllers/costlog_controller.rb
new file mode 100644
index 0000000000..e3aec77c0c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/controllers/costlog_controller.rb
@@ -0,0 +1,277 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostlogController < ApplicationController
+ menu_item :work_packages
+ before_filter :find_project, :authorize, only: [:edit,
+ :new,
+ :create,
+ :update,
+ :destroy]
+ before_filter :find_associated_objects, only: [:create,
+ :update]
+ before_filter :find_optional_project, only: [:report,
+ :index]
+
+ helper :sort
+ include SortHelper
+ helper :work_packages
+ include CostlogHelper
+ include PaginationHelper
+
+ def index
+ sort_init 'spent_on', 'desc'
+ sort_update 'spent_on' => 'spent_on',
+ 'user' => 'user_id',
+ 'project' => "#{Project.table_name}.name",
+ 'work_package' => 'work_package_id',
+ 'cost_type' => 'cost_type_id',
+ 'units' => 'units',
+ 'costs' => 'costs'
+
+ cond = ARCondition.new
+ if @project.nil?
+ cond << Project.allowed_to_condition(User.current, :view_cost_entries)
+ elsif @work_package.nil?
+ cond << @project.project_condition(Setting.display_subprojects_work_packages?)
+ else
+ root_cond = "#{WorkPackage.table_name}.root_id #{(@work_package.root_id.nil?) ? 'IS NULL' : "= #{@work_package.root_id}"}"
+ cond << "#{root_cond} AND #{WorkPackage.table_name}.lft >= #{@work_package.lft} AND #{WorkPackage.table_name}.rgt <= #{@work_package.rgt}"
+ end
+
+ if @cost_type
+ cond << ["#{CostEntry.table_name}.cost_type_id = ?", @cost_type.id]
+ end
+
+ retrieve_date_range
+ cond << ['spent_on BETWEEN ? AND ?', @from, @to]
+
+ respond_to do |format|
+ format.html {
+ @entries = CostEntry.includes(:project, :cost_type, :user, work_package: :type)
+ .merge(Project.allowed_to(User.current, :view_cost_entries, project: @project))
+ .where(cond.conditions)
+ .order(sort_clause)
+ .page(page_param)
+ .per_page(per_page_param)
+
+ render layout: !request.xhr?
+ }
+ end
+ end
+
+ def new
+ new_default_cost_entry
+
+ render action: 'edit'
+ end
+
+ def edit
+ render_403 unless @cost_entry.try(:editable_by?, User.current)
+ end
+
+ def create
+ new_default_cost_entry
+ update_cost_entry_from_params
+
+ if !@cost_entry.creatable_by?(User.current)
+
+ render_403
+
+ elsif @cost_entry.save
+
+ flash[:notice] = l(:notice_cost_logged_successfully)
+ redirect_back_or_default action: 'index', project_id: @cost_entry.project
+
+ else
+ render action: 'edit'
+ end
+ end
+
+ def update
+ update_cost_entry_from_params
+
+ if !@cost_entry.editable_by?(User.current)
+
+ render_403
+
+ elsif @cost_entry.save
+
+ flash[:notice] = l(:notice_successful_update)
+ redirect_back_or_default action: 'index', project_id: @cost_entry.project
+
+ else
+ render action: 'edit'
+ end
+ end
+
+ verify method: :delete, only: :destroy, render: { nothing: true, status: :method_not_allowed }
+ def destroy
+ render_404 and return unless @cost_entry
+ render_403 and return unless @cost_entry.editable_by?(User.current)
+ @cost_entry.destroy
+ flash[:notice] = l(:notice_successful_delete)
+
+ if request.referer =~ /cost_reports/
+ redirect_to controller: '/cost_reports', action: :index
+ else
+ redirect_to :back
+ end
+ rescue ::ActionController::RedirectBackError
+ redirect_to action: 'index', project_id: @cost_entry.project
+ end
+
+ def get_cost_type_unit_plural
+ @cost_type = CostType.find(params[:cost_type_id]) unless params[:cost_type_id].empty?
+
+ if request.xhr?
+ render partial: 'cost_type_unit_plural', layout: false
+ end
+ end
+
+ private
+
+ def find_project
+ # copied from timelog_controller.rb
+ if params[:id]
+ @cost_entry = CostEntry.find(params[:id])
+ @project = @cost_entry.project
+ elsif params[:work_package_id]
+ @work_package = WorkPackage.find(params[:work_package_id])
+ @project = @work_package.project
+ elsif params[:work_package_id]
+ @work_package = WorkPackage.find(params[:work_package_id])
+ @project = @work_package.project
+ elsif params[:project_id]
+ @project = Project.find(params[:project_id])
+ else
+ render_404
+ return false
+ end
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ if !params[:work_package_id].blank?
+ @work_package = WorkPackage.find(params[:work_package_id])
+ @project = @work_package.project
+ elsif !params[:work_package_id].blank?
+ @work_package = WorkPackage.find(params[:work_package_id])
+ @project = @work_package.project
+ elsif !params[:project_id].blank?
+ @project = Project.find(params[:project_id])
+ end
+
+ if !params[:cost_type_id].blank?
+ @cost_type = CostType.find(params[:cost_type_id])
+ end
+ end
+
+ def find_associated_objects
+ user_id = cost_entry_params.delete(:user_id)
+ @user = @cost_entry.present? && @cost_entry.user_id == user_id ?
+ @cost_entry.user :
+ User.find_by_id(user_id)
+
+ work_package_id = cost_entry_params.delete(:work_package_id)
+ @work_package = @cost_entry.present? && @cost_entry.work_package_id == work_package_id ?
+ @cost_entry.work_package :
+ WorkPackage.find_by_id(work_package_id)
+
+ cost_type_id = cost_entry_params.delete(:cost_type_id)
+ @cost_type = @cost_entry.present? && @cost_entry.cost_type_id == cost_type_id ?
+ @cost_entry.cost_type :
+ CostType.find_by_id(cost_type_id)
+ end
+
+ def retrieve_date_range
+ # Mostly copied from timelog_controller.rb
+ @free_period = false
+ @from = nil
+ @to = nil
+
+ if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
+ case params[:period].to_s
+ when 'today'
+ @from = @to = Date.today
+ when 'yesterday'
+ @from = @to = Date.today - 1
+ when 'current_week'
+ @from = Date.today - (Date.today.cwday - 1) % 7
+ @to = @from + 6
+ when 'last_week'
+ @from = Date.today - 7 - (Date.today.cwday - 1) % 7
+ @to = @from + 6
+ when '7_days'
+ @from = Date.today - 7
+ @to = Date.today
+ when 'current_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1)
+ @to = (@from >> 1) - 1
+ when 'last_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
+ @to = (@from >> 1) - 1
+ when '30_days'
+ @from = Date.today - 30
+ @to = Date.today
+ when 'current_year'
+ @from = Date.civil(Date.today.year, 1, 1)
+ @to = Date.civil(Date.today.year, 12, 31)
+ end
+ elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
+ begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
+ begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
+ @free_period = true
+ # default
+ end
+
+ @from, @to = @to, @from if @from && @to && @from > @to
+ @from ||= (CostEntry.includes([:project, :user])
+ .where(Project.allowed_to_condition(User.current, :view_cost_entries))
+ .minimum(:spent_on) || Date.today) - 1
+ @to ||= (CostEntry.includes([:project, :user])
+ .where(Project.allowed_to_condition(User.current, :view_cost_entries))
+ .maximum(:spent_on) || Date.today)
+ end
+
+ def new_default_cost_entry
+ @cost_entry = CostEntry.new.tap do |ce|
+ ce.project = @project
+ ce.work_package = @work_package
+ ce.user = User.current
+ ce.spent_on = Date.today
+ # notice that cost_type is set to default cost_type in the model
+ end
+ end
+
+ def update_cost_entry_from_params
+ @cost_entry.user = @user
+ @cost_entry.work_package = @work_package
+ @cost_entry.cost_type = @cost_type
+
+ @cost_entry.attributes = permitted_params.cost_entry
+ end
+
+private
+ def cost_entry_params
+ params.require(:cost_entry).permit(:work_package_id, :spent_on, :user_id,
+ :cost_type_id, :units, :comments)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/controllers/hourly_rates_controller.rb b/vendored-plugins/openproject-costs/app/controllers/hourly_rates_controller.rb
new file mode 100644
index 0000000000..f6e1d41956
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/controllers/hourly_rates_controller.rb
@@ -0,0 +1,167 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class HourlyRatesController < ApplicationController
+ helper :users
+ helper :sort
+ include SortHelper
+ helper :hourly_rates
+ include HourlyRatesHelper
+
+ before_filter :find_user, only: [:show, :edit, :update, :set_rate]
+
+ before_filter :find_optional_project, only: [:show, :edit, :update]
+ before_filter :find_project, only: [:set_rate]
+
+ # #show, #edit have their own authorization
+ before_filter :authorize, except: [:show, :edit, :update]
+
+ # TODO: this should be an index
+ def show
+ if @project
+ return deny_access unless User.current.allowed_to?(:view_hourly_rates, @project, for: @user)
+
+ @rates = HourlyRate.where(user_id: @user, project_id: @project)
+ .order("#{HourlyRate.table_name}.valid_from desc")
+ else
+ @rates = HourlyRate.history_for_user(@user)
+ @rates_default = @rates.delete(nil)
+ end
+ end
+
+ def edit
+ # TODO: split into edit and update
+ # remove code where appropriate
+ if @project
+ # Hourly Rate
+ return deny_access unless User.current.allowed_to?(:edit_hourly_rates, @project)
+ else
+ # Default Hourly Rate
+ return deny_access unless User.current.admin?
+ end
+
+ if @project.nil?
+ @rates = DefaultHourlyRate.where(user_id: @user)
+ .order("#{DefaultHourlyRate.table_name}.valid_from desc")
+ @rates << @user.default_rates.build(valid_from: Date.today) if @rates.empty?
+ else
+ @rates = @user.rates.select { |r| r.project_id == @project.id }.sort { |a, b| b.valid_from <=> a.valid_from }
+ @rates << @user.rates.build(valid_from: Date.today, project: @project) if @rates.empty?
+ end
+
+ render action: 'edit', layout: !request.xhr?
+ end
+
+ def update
+ # TODO: copied over from edit
+ # remove code where appropriate
+ if @project
+ # Hourly Rate
+ return deny_access unless User.current.allowed_to?(:edit_hourly_rates, @project)
+ else
+ # Default Hourly Rate
+ return deny_access unless User.current.admin?
+ end
+
+ if params.include? 'user'
+ update_rates @user,
+ @project,
+ permitted_params.user_rates[:new_rate_attributes],
+ permitted_params.user_rates[:existing_rate_attributes]
+ else
+ delete_rates @user, @project
+ end
+
+ if @user.save
+ flash[:notice] = l(:notice_successful_update)
+ if @project.nil?
+ redirect_back_or_default(controller: 'users', action: 'edit', id: @user)
+ else
+ redirect_back_or_default(action: 'show', id: @user, project_id: @project)
+ end
+ else
+ if @project.nil?
+ @rates = @user.default_rates
+ @rates << @user.default_rates.build(valid_from: Date.today) if @rates.empty?
+ else
+ @rates = @user.rates.select { |r| r.project_id == @project.id }.sort { |a, b| b.valid_from <=> a.valid_from }
+ @rates << @user.rates.build(valid_from: Date.today, project: @project) if @rates.empty?
+ end
+ render action: 'edit', layout: !request.xhr?
+ end
+ end
+
+ def set_rate
+ today = Date.today
+
+ rate = @user.rate_at(today, @project)
+ rate = HourlyRate.new if rate.nil? || rate.valid_from != today
+
+ rate.tap do |hr|
+ hr.project = @project
+ hr.user = @user
+ hr.valid_from = today
+ hr.rate = clean_currency(params[:rate])
+ end
+
+ if rate.save
+ if request.xhr?
+ render :update do |page|
+ page.replace_html "rate_for_#{@user.id}", link_to(number_to_currency(rate.rate), action: 'edit', id: @user, project_id: @project)
+ end
+ else
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to action: 'index'
+ end
+ end
+ end
+
+ private
+
+ def update_rates(user, project, added_rates, changed_rates)
+ user.add_rates(project, added_rates)
+ user.set_existing_rates(project, changed_rates)
+ end
+
+ def delete_rates(user, project)
+ if project.present?
+ user.rates.delete_all
+ else
+ user.default_rates.delete_all
+ end
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ @project = params[:project_id].blank? ? nil : Project.find(params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_user
+ @user = params[:id] ? User.find(params[:id]) : User.current
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/helpers/cost_objects_helper.rb b/vendored-plugins/openproject-costs/app/helpers/cost_objects_helper.rb
new file mode 100644
index 0000000000..e53116ad86
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/helpers/cost_objects_helper.rb
@@ -0,0 +1,65 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'csv'
+
+module CostObjectsHelper
+ include ApplicationHelper
+
+ # Check if the current user is allowed to manage the budget. Based on Role
+ # permissions.
+ def allowed_management?
+ User.current.allowed_to?(:edit_cost_objects, @project)
+ end
+
+ def cost_objects_to_csv(cost_objects)
+ CSV.generate(col_sep: l(:general_csv_separator)) do |csv|
+ # csv header fields
+ headers = ['#',
+ Project.model_name.human,
+ CostObject.human_attribute_name(:subject),
+ CostObject.human_attribute_name(:author),
+ CostObject.human_attribute_name(:fixed_date),
+ VariableCostObject.human_attribute_name(:material_budget),
+ VariableCostObject.human_attribute_name(:labor_budget),
+ CostObject.human_attribute_name(:spent),
+ CostObject.human_attribute_name(:created_on),
+ CostObject.human_attribute_name(:updated_on),
+ CostObject.human_attribute_name(:description)
+ ]
+ csv << headers.map { |c| begin; c.to_s.encode('UTF-8'); rescue; c.to_s; end }
+ # csv lines
+ cost_objects.each do |cost_object|
+ fields = [cost_object.id,
+ cost_object.project.name,
+ cost_object.subject,
+ cost_object.author.name,
+ format_date(cost_object.fixed_date),
+ cost_object.kind == 'VariableCostObject' ? number_to_currency(cost_object.material_budget) : '',
+ cost_object.kind == 'VariableCostObject' ? number_to_currency(cost_object.labor_budget) : '',
+ cost_object.kind == 'VariableCostObject' ? number_to_currency(cost_object.spent) : '',
+ format_time(cost_object.created_on),
+ format_time(cost_object.updated_on),
+ cost_object.description
+ ]
+ csv << fields.map { |c| begin; c.to_s.encode('UTF-8'); rescue; c.to_s; end }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/helpers/cost_types_helper.rb b/vendored-plugins/openproject-costs/app/helpers/cost_types_helper.rb
new file mode 100644
index 0000000000..ec991e3db4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/helpers/cost_types_helper.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostTypesHelper
+ include CostlogHelper
+end
diff --git a/vendored-plugins/openproject-costs/app/helpers/costlog_helper.rb b/vendored-plugins/openproject-costs/app/helpers/costlog_helper.rb
new file mode 100644
index 0000000000..9f560b570a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/helpers/costlog_helper.rb
@@ -0,0 +1,78 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostlogHelper
+ include TimelogHelper
+
+ def render_costlog_breadcrumb
+ links = []
+ links << link_to(l(:label_project_all), project_id: nil, work_package_id: nil)
+ links << link_to(h(@project), project_id: @project, work_package_id: nil) if @project
+ links << link_to_work_package(@work_package, subject: false) if @work_package
+ breadcrumb links
+ end
+
+ def cost_types_collection_for_select_options(selected_type = nil)
+ cost_types = CostType.active.sort
+
+ if selected_type && !cost_types.include?(selected_type)
+ cost_types << selected_type
+ cost_types.sort
+ end
+ collection = []
+ collection << ["--- #{l(:actionview_instancetag_blank_option)} ---", ''] unless cost_types.detect(&:is_default?)
+ cost_types.each do |t| collection << [t.name, t.id] end
+ collection
+ end
+
+ def user_collection_for_select_options(_options = {})
+ users = @project.possible_assignees
+ collection = []
+ users.each do |u| collection << [u.name, u.id] end
+ collection
+ end
+
+ def extended_progress_bar(pcts, options = {})
+ return progress_bar(pcts, options) unless pcts.is_a?(Numeric) && pcts > 100
+
+ width = options[:width] || '100px;'
+ legend = options[:legend] || ''
+ content_tag('table',
+ content_tag('tr',
+ content_tag('td', '', style: "width: #{((100.0 / pcts) * 100).round}%;", class: 'closed') +
+ content_tag('td', '', style: "width: #{100.0 - ((100.0 / pcts) * 100).round}%;", class: 'exceeded')
+ ), class: 'progress', style: "width: #{width};") +
+ content_tag('p', legend, class: 'pourcent')
+ end
+
+ def clean_currency(value)
+ return nil if value.nil? || value == ''
+
+ value = value.strip
+ value.gsub!(l(:currency_delimiter), '') if value.include?(l(:currency_delimiter)) && value.include?(l(:currency_separator))
+ value.gsub(',', '.')
+ BigDecimal.new(value)
+ end
+
+ def to_currency_with_empty(rate)
+ rate.nil? ?
+ '0.0' :
+ number_to_currency(rate.rate)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/helpers/hourly_rates_helper.rb b/vendored-plugins/openproject-costs/app/helpers/hourly_rates_helper.rb
new file mode 100644
index 0000000000..aaeba63b6f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/helpers/hourly_rates_helper.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module HourlyRatesHelper
+ include CostlogHelper
+
+ # Returns the rate that is the closest at the specified date and that is
+ # defined in the specified projects or it's ancestors. The ancestor chain
+ # is traversed from the specified project upwards.
+ #
+ # Expects all_rates to be all the rates that the user possibly has
+ # grouped by project typically by having called HourlyRates.history_for_user
+ #
+ # This is faster than calling current_rate for each project
+ def at_date_in_project_with_ancestors(at_date, all_rates, project)
+ self_and_ancestors = all_rates.keys
+ .select { |ancestor| ancestor.lft <= project.lft && ancestor.rgt >= project.rgt }
+ .sort_by(&:lft)
+ .reverse
+
+ self_and_ancestors.each do |ancestor|
+ rate = all_rates[ancestor].select { |rate| rate.valid_from <= at_date }
+ .sort_by(&:valid_from)
+ .last
+
+ return rate if rate
+ end
+
+ return nil
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/activity/cost_object_activity_provider.rb b/vendored-plugins/openproject-costs/app/models/activity/cost_object_activity_provider.rb
new file mode 100644
index 0000000000..e0e6e7eff4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/activity/cost_object_activity_provider.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Activity::CostObjectActivityProvider < Activity::BaseActivityProvider
+ acts_as_activity_provider type: 'cost_objects',
+ permission: :view_cost_objects
+
+ def event_query_projection(activity)
+ [
+ activity_journal_projection_statement(:subject, 'cost_object_subject', activity),
+ activity_journal_projection_statement(:project_id, 'project_id', activity)
+ ]
+ end
+
+ def event_type(_event, _activity)
+ 'cost_object'
+ end
+
+ def event_title(event, _activity)
+ "#{l(:label_cost_object)} ##{event['journable_id']}: #{event['cost_object_subject']}"
+ end
+
+ def event_path(event, _activity)
+ url_helpers.cost_object_path(url_helper_parameter(event))
+ end
+
+ def event_url(event, _activity)
+ url_helpers.cost_object_url(url_helper_parameter(event))
+ end
+
+ private
+
+ def url_helper_parameter(event)
+ event['journable_id']
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/cost_entry.rb b/vendored-plugins/openproject-costs/app/models/cost_entry.rb
new file mode 100644
index 0000000000..3ee6fa8f71
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/cost_entry.rb
@@ -0,0 +1,176 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostEntry < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :work_package
+ belongs_to :user
+ include ::OpenProject::Costs::DeletedUserFallback
+ belongs_to :cost_type
+ belongs_to :cost_object
+ belongs_to :rate, class_name: 'CostRate'
+
+ include ActiveModel::ForbiddenAttributesProtection
+
+ validates_presence_of :work_package_id, :project_id, :user_id, :cost_type_id, :units, :spent_on
+ validates_numericality_of :units, allow_nil: false, message: :invalid
+ validates_length_of :comments, maximum: 255, allow_nil: true
+
+ before_save :before_save
+ before_validation :before_validation
+ after_initialize :after_initialize
+ validate :validate
+
+ scope :visible, lambda { |*args|
+ where(CostEntry.visible_condition(args[0] || User.current, args[1]))
+ .includes([:project, :user])
+ .references(:project)
+ }
+
+ scope :on_work_packages, ->(work_packages) { where(work_package_id: work_packages) }
+
+ def self.visible_condition(user, project)
+ %{ (#{Project.allowed_to_condition(user, :view_cost_entries, project: project)} OR
+ (#{Project.allowed_to_condition(user, :view_own_cost_entries, project: project)} AND #{CostEntry.table_name}.user_id = #{user.id})) }
+ end
+
+ scope :visible_costs, lambda{|*args|
+ view_cost_rates = Project.allowed_to_condition((args.first || User.current), :view_cost_rates, project: args[1])
+ view_cost_entries = CostEntry.visible_condition((args.first || User.current), args[1])
+
+ where([view_cost_entries, view_cost_rates].join(' AND '))
+ .includes([:project, :user])
+ }
+
+ def self.costs_of(work_packages:)
+ # N.B. Because of an AR quirks the code below uses statements like
+ # where(work_package_id: ids)
+ # You would expect to be able to simply write those as
+ # where(work_package: work_packages)
+ # However, AR (Rails 4.2) will not expand :includes + :references inside a subquery,
+ # which will render the query invalid. Therefore we manually extract the IDs in a separate (pluck) query.
+ ids = if work_packages.respond_to?(:pluck)
+ work_packages.pluck(:id)
+ else
+ Array(work_packages).map { |wp| wp.id }
+ end
+ CostEntry.where(work_package_id: ids)
+ .joins(work_package: :project)
+ .visible_costs
+ .sum("COALESCE(#{CostEntry.table_name}.overridden_costs,
+ #{CostEntry.table_name}.costs)").to_f
+ end
+
+ def after_initialize
+ if new_record? && cost_type.nil?
+ if default_cost_type = CostType.default
+ self.cost_type_id = default_cost_type.id
+ end
+ end
+ end
+
+ def before_validation
+ self.project = work_package.project if work_package && project.nil?
+ end
+
+ def validate
+ errors.add :units, :invalid if units && (units < 0)
+ errors.add :project_id, :invalid if project.nil?
+ errors.add :work_package_id, :invalid if work_package.nil? || (project != work_package.project)
+ errors.add :cost_type_id, :invalid if cost_type.present? && cost_type.deleted_at.present?
+ errors.add :user_id, :invalid if project.present? && !project.users.include?(user) && user_id_changed?
+
+ begin
+ spent_on.to_date
+ rescue Exception
+ errors.add :spent_on, :invalid
+ end
+ end
+
+ def before_save
+ self.spent_on &&= spent_on.to_date
+ update_costs
+ end
+
+ def overwritten_costs=(costs)
+ write_attribute(:overwritten_costs, CostRate.clean_currency(costs))
+ end
+
+ def units=(units)
+ write_attribute(:units, CostRate.clean_currency(units))
+ end
+
+ # tyear, tmonth, tweek assigned where setting spent_on attributes
+ # these attributes make time aggregations easier
+ def spent_on=(date)
+ super
+ self.tyear = spent_on ? spent_on.year : nil
+ self.tmonth = spent_on ? spent_on.month : nil
+ self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
+ end
+
+ def real_costs
+ # This methods returns the actual assigned costs of the entry
+ overridden_costs || costs || calculated_costs
+ end
+
+ def calculated_costs(rate_attr = nil)
+ rate_attr ||= current_rate
+ units * rate_attr.rate
+ rescue
+ 0.0
+ end
+
+ def update_costs(rate_attr = nil)
+ rate_attr ||= current_rate
+ if rate_attr.nil?
+ self.costs = 0.0
+ self.rate = nil
+ return
+ end
+
+ self.costs = calculated_costs(rate_attr)
+ self.rate = rate_attr
+ end
+
+ def update_costs!(rate_attr = nil)
+ update_costs(rate_attr)
+ self.save!
+ end
+
+ def current_rate
+ cost_type.rate_at(self.spent_on)
+ end
+
+ # Returns true if the cost entry can be edited by usr, otherwise false
+ def editable_by?(usr)
+ usr.allowed_to?(:edit_cost_entries, project) ||
+ (usr.allowed_to?(:edit_own_cost_entries, project) && user_id == usr.id)
+ end
+
+ def creatable_by?(usr)
+ usr.allowed_to?(:log_costs, project) ||
+ (usr.allowed_to?(:log_own_costs, project) && user_id == usr.id)
+ end
+
+ def costs_visible_by?(usr)
+ usr.allowed_to?(:view_cost_rates, project) ||
+ (usr.id == user_id && !overridden_costs.nil?)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/cost_object.rb b/vendored-plugins/openproject-costs/app/models/cost_object.rb
new file mode 100644
index 0000000000..e8c39f79ab
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/cost_object.rb
@@ -0,0 +1,141 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# A CostObject is an item that is created as part of the project. These items
+# contain a collection of work packages.
+class CostObject < ActiveRecord::Base
+ belongs_to :author, class_name: 'User', foreign_key: 'author_id'
+ belongs_to :project
+ has_many :work_packages, dependent: :nullify
+
+ has_many :cost_entries, through: :work_packages
+ has_many :time_entries, through: :work_packages
+
+ include ActiveModel::ForbiddenAttributesProtection
+
+ acts_as_attachable after_remove: :attachment_removed
+
+ acts_as_journalized
+
+ acts_as_event type: 'cost-objects',
+ title: Proc.new { |o| "#{l(:label_cost_object)} ##{o.id}: #{o.subject}" },
+ url: Proc.new { |o| { controller: 'cost_objects', action: 'show', id: o.id } }
+
+ validates_presence_of :subject, :project, :author, :kind, :fixed_date
+ validates_length_of :subject, maximum: 255
+ validates_length_of :subject, minimum: 1
+
+ User.before_destroy do |user|
+ CostObject.replace_author_with_deleted_user user
+ end
+
+ def initialize(attributes = nil)
+ super
+ self.author = User.current if self.new_record?
+ end
+
+ def copy_from(arg)
+ if !arg.is_a?(Hash)
+ # turn args into an attributes hash if it is not already (which is the case when called from VariableCostObject)
+ arg = (arg.is_a?(CostObject) ? arg : self.class.find(arg)).attributes.dup
+ end
+ arg.delete('id')
+ self.type = arg.delete('type')
+ self.attributes = arg
+ end
+
+ # Wrap type column to make it usable in views (especially in a select tag)
+ def kind
+ self[:type]
+ end
+
+ def kind=(type)
+ self[:type] = type
+ end
+
+ # Assign all the work_packages with +version_id+ to this Cost Object
+ def assign_work_packages_by_version(version_id)
+ version = Version.find_by_id(version_id)
+ return 0 if version.nil? || version.fixed_work_packages.blank?
+
+ version.fixed_work_packages.each do |work_package|
+ work_package.update_attribute(:cost_object_id, id)
+ end
+
+ version.fixed_work_packages.size
+ end
+
+ # Change the Cost Object type to another type. Valid types are
+ #
+ # * FixedCostObject
+ # * VariableCostObject
+ def change_type(to)
+ if [FixedCostObject.name, VariableCostObject.name].include?(to)
+ self.type = to
+ self.save!
+ return CostObject.find(id)
+ else
+ return self
+ end
+ end
+
+ # Amount spent. Virtual accessor that is overriden by subclasses.
+ def spent
+ 0
+ end
+
+ # Budget of labor. Virtual accessor that is overriden by subclasses.
+ def labor_budget
+ 0.0
+ end
+
+ # Budget of material, i.e. all costs besides labor costs. Virtual accessor that is overriden by subclasses.
+ def material_budget
+ 0.0
+ end
+
+ def budget
+ material_budget + labor_budget
+ end
+
+ # Label of the current type for display in GUI. Virtual accessor that is overriden by subclasses.
+ def type_label
+ l(:label_cost_object)
+ end
+
+ # Amount of the budget spent. Expressed as as a percentage whole number
+ def budget_ratio
+ return 0.0 if budget.nil? || budget == 0.0
+ ((spent / budget) * 100).round
+ end
+
+ def css_classes
+ 'cost_object'
+ end
+
+ def self.replace_author_with_deleted_user(user)
+ substitute = DeletedUser.first
+
+ where(author_id: user.id).update_all(author_id: substitute.id)
+ end
+
+ def to_s
+ subject
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/cost_rate.rb b/vendored-plugins/openproject-costs/app/models/cost_rate.rb
new file mode 100644
index 0000000000..96272d1951
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/cost_rate.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostRate < Rate
+ belongs_to :cost_type
+
+ validates_uniqueness_of :valid_from, scope: :cost_type_id
+ validate :change_of_cost_type_only_on_first_creation
+
+ def previous(reference_date = valid_from)
+ # This might return a default rate
+ cost_type.rate_at(reference_date - 1)
+ end
+
+ def next(reference_date = valid_from)
+ CostRate
+ .where(['cost_type_id = ? and valid_from > ?', cost_type_id, reference_date])
+ .order('valid_from ASC')
+ .first
+ end
+
+ private
+
+ def change_of_cost_type_only_on_first_creation
+ errors.add :cost_type_id, :invalid if cost_type_id_changed? && !self.new_record?
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/cost_type.rb b/vendored-plugins/openproject-costs/app/models/cost_type.rb
new file mode 100644
index 0000000000..f5f0bc1e9b
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/cost_type.rb
@@ -0,0 +1,93 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostType < ActiveRecord::Base
+ has_many :material_budget_items
+ has_many :cost_entries, dependent: :destroy
+ has_many :rates, class_name: 'CostRate', foreign_key: 'cost_type_id', dependent: :destroy
+
+ validates_presence_of :name, :unit, :unit_plural
+ validates_uniqueness_of :name
+
+ after_update :save_rates
+
+ include ActiveModel::ForbiddenAttributesProtection
+
+ scope :active, -> { where(deleted_at: nil) }
+
+ # finds the default CostType
+ def self.default
+ CostType.find_by(default: true) || CostType.first
+ end
+
+ def is_default?
+ default
+ end
+
+ def <=>(cost_type)
+ name.downcase <=> cost_type.name.downcase
+ end
+
+ def current_rate
+ rate_at(Date.today)
+ end
+
+ def rate_at(date)
+ CostRate.where(['cost_type_id = ? and valid_from <= ?', id, date])
+ .order('valid_from DESC')
+ .first
+ end
+
+ def visible?(user)
+ user.admin?
+ end
+
+ def to_s
+ name
+ end
+
+ def new_rate_attributes=(rate_attributes)
+ rate_attributes.each do |_index, attributes|
+ attributes[:rate] = Rate.clean_currency(attributes[:rate])
+ rates.build(attributes)
+ end
+ end
+
+ def existing_rate_attributes=(rate_attributes)
+ rates.reject(&:new_record?).each do |rate|
+ attributes = rate_attributes[rate.id.to_s]
+
+ has_rate = false
+ if attributes && attributes[:rate].present?
+ attributes[:rate] = Rate.clean_currency(attributes[:rate])
+ has_rate = true
+ end
+
+ if has_rate
+ rate.attributes = attributes
+ else
+ rates.delete(rate)
+ end
+ end
+ end
+
+ def save_rates
+ rates.each(&:save!)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/costs_work_package_observer.rb b/vendored-plugins/openproject-costs/app/models/costs_work_package_observer.rb
new file mode 100644
index 0000000000..3842cb8da3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/costs_work_package_observer.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostsWorkPackageObserver < ActiveRecord::Observer
+ observe :work_package
+
+ def after_update(work_package)
+ if work_package.project_id_changed?
+ # TODO: This only works with the global cost_rates
+ CostEntry
+ .where(work_package_id: work_package.id)
+ .update_all(project_id: work_package.project_id)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/default_hourly_rate.rb b/vendored-plugins/openproject-costs/app/models/default_hourly_rate.rb
new file mode 100644
index 0000000000..f845e5c3d3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/default_hourly_rate.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class DefaultHourlyRate < Rate
+ validates_uniqueness_of :valid_from, scope: :user_id
+ validates_presence_of :user_id, :valid_from
+ validate :change_of_user_only_on_first_creation
+ before_save :convert_valid_from_to_date
+
+ def next(reference_date = valid_from)
+ DefaultHourlyRate
+ .where(['user_id = ? and valid_from > ?', user_id, reference_date])
+ .order('valid_from ASC')
+ .first
+ end
+
+ def previous(reference_date = valid_from)
+ user.default_rate_at(reference_date - 1)
+ end
+
+ def self.at_for_user(date, user_id)
+ user_id = user_id.id if user_id.is_a?(User)
+
+ where(['user_id = ? and valid_from <= ?', user_id, date]).order('valid_from DESC').first
+ end
+
+ private
+
+ def convert_valid_from_to_date
+ self.valid_from &&= valid_from.to_date
+ end
+
+ def change_of_user_only_on_first_creation
+ # Only allow change of user on first creation
+ errors.add :user_id, :invalid if !self.new_record? and user_id_changed?
+ begin
+ valid_from.to_date
+ rescue Exception
+ errors.add :valid_from, :invalid
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/default_hourly_rate_observer.rb b/vendored-plugins/openproject-costs/app/models/default_hourly_rate_observer.rb
new file mode 100644
index 0000000000..e553d76c96
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/default_hourly_rate_observer.rb
@@ -0,0 +1,105 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class DefaultHourlyRateObserver < ActiveRecord::Observer
+ class Methods
+ def initialize(changed_rate)
+ @rate = changed_rate
+ end
+
+ def order_dates(date1, date2)
+ # order the dates
+ return date1 || date2 if date1.nil? || date2.nil?
+
+ if date2 < date1
+ date_tmp = date2
+ date2 = date1
+ date1 = date_tmp
+ end
+ [date1, date2]
+ end
+
+ def orphaned_child_entries(date1, date2 = nil)
+ # This method returns all entries in all projects without an explicit rate
+ # between date1 and date2
+ # i.e. the ones with an assigned default rate or without a rate
+
+ (date1, date2) = order_dates(date1, date2)
+
+ # This gets an array of all the ids of the DefaultHourlyRates
+ default_rates = DefaultHourlyRate.pluck(:id)
+
+ if date1.nil? || date2.nil?
+ # we have only one date, query >=
+ conditions = [
+ 'user_id = ? AND (rate_id IN (?) OR rate_id IS NULL) AND spent_on >= ?',
+ @rate.user_id, default_rates, date1 || date2
+ ]
+ else
+ # we have two dates, query between
+ conditions = [
+ 'user_id = ? AND (rate_id IN (?) OR rate_id IS NULL) AND spent_on BETWEEN ? AND ?',
+ @rate.user_id, default_rates, date1, date2 - 1
+ ]
+ end
+
+ TimeEntry.includes(:rate).where(conditions)
+ end
+
+ def update_entries(entries, rate = @rate)
+ # This methods updates the given array of time or cost entries with the given rate
+ entries = [entries] unless entries.respond_to?(:each)
+ ActiveRecord::Base.cache do
+ entries.each do |entry|
+ entry.update_costs!(rate)
+ end
+ end
+ end
+ end
+
+ def after_create(rate)
+ o = Methods.new(rate)
+
+ next_rate = rate.next
+ # and entries from all projects that need updating
+ entries = o.orphaned_child_entries(rate.valid_from, (next_rate.valid_from if next_rate))
+
+ o.update_entries(entries)
+ end
+
+ def after_update(rate)
+ # FIXME: This might be extremly slow. Consider using an implementation like in HourlyRateObserver
+ unless rate.valid_from_changed?
+ # We have not moved a rate, maybe just changed the rate value
+
+ return unless rate.rate_changed?
+ # Only the rate value was changed so just update the currently assigned entries
+ return after_create(rate)
+ end
+
+ after_destroy(rate)
+ after_create(rate)
+ end
+
+ def after_destroy(rate)
+ o = Methods.new(rate)
+
+ o.update_entries(TimeEntry.where(rate_id: rate.id))
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/entry.rb b/vendored-plugins/openproject-costs/app/models/entry.rb
new file mode 100644
index 0000000000..04d9500d81
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/entry.rb
@@ -0,0 +1,101 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Entry
+ [TimeEntry, CostEntry].each do |e| e.send :include, self end
+
+ class Delegator < ActiveRecord::Base
+ self.abstract_class = true
+ class << self
+ def ===(obj)
+ TimeEntry === obj or CostEntry === obj
+ end
+
+ def calculate(type, *args)
+ a = TimeEntry.calculate(type, *args)
+ b = CostEntry.calculate(type, *args)
+ case type
+ when :sum, :count then a + b
+ when :avg then (a + b) / 2
+ when :min then [a, b].min
+ when :max then [a, b].max
+ else raise NotImplementedError
+ end
+ end
+
+ %w[find_by_sql count_by_sql count sum].each do |meth|
+ define_method(meth) { |*args| find_all(meth, *args) }
+ end
+
+ undef_method :create, :update, :delete, :destroy, :new, :update_counters,
+ :increment_counter, :decrement_counter
+
+ %w[update_all destroy_all delete_all].each do |meth|
+ define_method(meth) { |*args| send_all(meth, *args) }
+ end
+
+ private
+
+ def find_initial(options) find_one :find_initial, options end
+
+ def find_last(options) find_one :find_last, options end
+
+ def find_every(options) find_many :find_every, options end
+
+ def find_from_ids(_args, options) find_many :find_from_ids, options end
+
+ def find_one(*args)
+ TimeEntry.send(*args) || CostEntry.send(*args)
+ end
+
+ def find_many(*args)
+ TimeEntry.send(*args) + CostEntry.send(*args)
+ end
+
+ def send_all(*args)
+ [TimeEntry.send(*args), CostEntry.send(*args)]
+ end
+ end
+ end
+
+ def units
+ super
+ rescue NoMethodError
+ hours
+ end
+
+ def cost_type
+ super
+ rescue NoMethodError
+ end
+
+ def activity
+ super
+ rescue NoMethodError
+ end
+
+ def activity_id
+ super
+ rescue NoMethodError
+ end
+
+ def self.method_missing(*a, &b)
+ Delegator.send(*a, &b)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/fixed_cost_object.rb b/vendored-plugins/openproject-costs/app/models/fixed_cost_object.rb
new file mode 100644
index 0000000000..934c2f9c24
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/fixed_cost_object.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class FixedCostObject < CostObject
+ # Label of the current type for display in GUI.
+ def type_label
+ l(:label_fixed_cost_object)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/hourly_rate.rb b/vendored-plugins/openproject-costs/app/models/hourly_rate.rb
new file mode 100644
index 0000000000..65797ffa0e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/hourly_rate.rb
@@ -0,0 +1,98 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class HourlyRate < Rate
+ validates_uniqueness_of :valid_from, scope: [:user_id, :project_id]
+ validates_presence_of :user_id, :project_id, :valid_from
+ validate :change_of_user_only_on_first_creation
+
+ def previous(reference_date = valid_from)
+ # This might return a default rate
+ user.rate_at(reference_date - 1, project)
+ end
+
+ def next(reference_date = valid_from)
+ HourlyRate
+ .where(['user_id = ? and project_id = ? and valid_from > ?',
+ user_id, project_id, reference_date])
+ .order('valid_from ASC')
+ .first
+ end
+
+ def self.history_for_user(usr)
+ projects_with_costs_module = Project.has_module(:costs_module)
+ .active
+ .visible
+ .order(:name)
+
+ permitted_projects = Project.has_module(:costs_module)
+ .active
+ .allowed_to(User.current, :view_hourly_rates)
+
+ rates_by_project = HourlyRate.where(user_id: usr, project_id: permitted_projects)
+ .includes(:project)
+ .order("#{HourlyRate.table_name}.valid_from desc")
+ .group_by(&:project)
+
+ rates = {}
+
+ projects_with_costs_module.each do |project|
+ rates[project] = rates_by_project.fetch(project, [])
+ end
+
+ # FIXME: What permissions to apply here?
+ rates[nil] = DefaultHourlyRate
+ .where(user_id: usr)
+ .order("#{DefaultHourlyRate.table_name}.valid_from desc")
+
+ rates
+ end
+
+ def self.at_date_for_user_in_project(date, user_id, project = nil, include_default = true)
+ user_id = user_id.id if user_id.is_a?(User)
+
+ unless project.nil?
+ rate = where(['user_id = ? and project_id = ? and valid_from <= ?', user_id, project, date])
+ .order('valid_from DESC')
+ .first
+ if rate.nil?
+ project = Project.find(project) unless project.is_a?(Project)
+ rate = where(['user_id = ? and project_id in (?) and valid_from <= ?',
+ user_id,
+ project.ancestors.to_a,
+ date])
+ .includes(:project)
+ .order('projects.lft DESC, valid_from DESC')
+ .first
+ end
+ end
+ rate ||= DefaultHourlyRate.at_for_user(date, user_id) if include_default
+ rate
+ end
+
+ private
+
+ def change_of_user_only_on_first_creation
+ # Only allow change of project and user on first creation
+ return if self.new_record?
+
+ errors.add :project_id, :invalid if project_id_changed?
+ errors.add :user_id, :invalid if user_id_changed?
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/journal/cost_object_journal.rb b/vendored-plugins/openproject-costs/app/models/journal/cost_object_journal.rb
new file mode 100644
index 0000000000..b2ee983f86
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/journal/cost_object_journal.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Journal::CostObjectJournal < Journal::BaseJournal
+ self.table_name = 'cost_object_journals'
+end
diff --git a/vendored-plugins/openproject-costs/app/models/labor_budget_item.rb b/vendored-plugins/openproject-costs/app/models/labor_budget_item.rb
new file mode 100644
index 0000000000..dcc3921ea9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/labor_budget_item.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class LaborBudgetItem < ActiveRecord::Base
+ belongs_to :cost_object
+ belongs_to :user
+ include ::OpenProject::Costs::DeletedUserFallback
+
+ validates_length_of :comments, maximum: 255, allow_nil: true
+ validates_presence_of :user
+ validates_presence_of :cost_object
+ validates_numericality_of :hours, allow_nil: false
+
+ include ActiveModel::ForbiddenAttributesProtection
+ # user_id correctness is ensured in VariableCostObject#*_labor_budget_item_attributes=
+
+ def self.visible_condition(user, project)
+ %{ (#{Project.allowed_to_condition(user,
+ :view_hourly_rates,
+ project: project)} OR
+ (#{Project.allowed_to_condition(user,
+ :view_own_hourly_rate,
+ project: project)} AND #{LaborBudgetItem.table_name}.user_id = #{user.id})) }
+ end
+
+ scope :visible_costs, lambda{|*args|
+ includes([{ cost_object: :project }, :user])
+ .where(LaborBudgetItem.visible_condition((args.first || User.current), args[1]))
+ .references(:projects)
+ }
+
+ def costs
+ budget || calculated_costs
+ end
+
+ def calculated_costs(fixed_date = cost_object.fixed_date, project_id = cost_object.project_id)
+ if user_id && hours && rate = HourlyRate.at_date_for_user_in_project(fixed_date, user_id, project_id)
+ rate.rate * hours
+ else
+ 0.0
+ end
+ end
+
+ def costs_visible_by?(usr)
+ usr.allowed_to?(:view_hourly_rates, cost_object.project) ||
+ (usr.id == user_id && usr.allowed_to?(:view_own_hourly_rate, cost_object.project))
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/material_budget_item.rb b/vendored-plugins/openproject-costs/app/models/material_budget_item.rb
new file mode 100644
index 0000000000..d29c4cb5ad
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/material_budget_item.rb
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class MaterialBudgetItem < ActiveRecord::Base
+ belongs_to :cost_object
+ belongs_to :cost_type
+
+ validates_length_of :comments, maximum: 255, allow_nil: true
+ validates_presence_of :cost_type
+
+ include ActiveModel::ForbiddenAttributesProtection
+
+ def self.visible_condition(user, project)
+ Project.allowed_to_condition(user,
+ :view_cost_rates,
+ project: project)
+ end
+
+ scope :visible_costs, lambda { |*args|
+ where(MaterialBudgetItem.visible_condition((args.first || User.current), args[1]))
+ .includes(cost_object: :project)
+ .references(:projects)
+ }
+
+ def costs
+ budget || calculated_costs
+ end
+
+ def calculated_costs(fixed_date = cost_object.fixed_date)
+ if units && cost_type && rate = cost_type.rate_at(fixed_date)
+ rate.rate * units
+ else
+ 0.0
+ end
+ end
+
+ def costs_visible_by?(usr)
+ usr.allowed_to?(:view_cost_rates, cost_object.project)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/rate.rb b/vendored-plugins/openproject-costs/app/models/rate.rb
new file mode 100644
index 0000000000..8561cd6fa6
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/rate.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Rate < ActiveRecord::Base
+ validates_numericality_of :rate, allow_nil: false
+ validate :validate_date_is_a_date
+
+ before_save :convert_valid_from_to_date
+
+ belongs_to :user
+ include ::OpenProject::Costs::DeletedUserFallback
+ belongs_to :project
+
+ include ActiveModel::ForbiddenAttributesProtection
+
+ def self.clean_currency(value)
+ if value && value.is_a?(String)
+ value = value.strip
+ value.gsub!(l(:currency_delimiter), '') if value.include?(l(:currency_delimiter)) && value.include?(l(:currency_separator))
+ value.gsub(',', '.')
+ else
+ value
+ end
+ end
+
+ private
+
+ def convert_valid_from_to_date
+ self.valid_from &&= valid_from.to_date
+ end
+
+ def validate_date_is_a_date
+ valid_from.to_date
+ rescue Exception
+ errors.add :valid_from, :not_a_date
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/rate_observer.rb b/vendored-plugins/openproject-costs/app/models/rate_observer.rb
new file mode 100644
index 0000000000..bd4661c878
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/rate_observer.rb
@@ -0,0 +1,196 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class RateObserver < ActiveRecord::Observer
+ observe :hourly_rate, :cost_rate
+
+ class Methods
+ def initialize(changed_rate)
+ @rate = changed_rate
+ end
+
+ # order the dates
+ def order_dates(*dates)
+ dates.compact!
+ dates.size == 1 ? dates.first : dates.sort
+ end
+
+ def conditions_after(date, date_column = :spent_on)
+ if @rate.is_a?(HourlyRate)
+ [
+ "#{date_column} >= ? AND user_id = ? and project_id = ?",
+ date, @rate.user_id, @rate.project_id
+ ]
+ else
+ [
+ "#{date_column} >= ? AND cost_type_id = ?",
+ date, @rate.cost_type_id
+ ]
+ end
+ end
+
+ def conditions_between(date1, date2 = nil, date_column = :spent_on)
+ # if the second date is not given, return all entries
+ # with a spent_on after the given date
+ return conditions_after(date1 || date2, date_column) if date1.nil? || date2.nil?
+
+ (date1, date2) = order_dates(date1, date2)
+
+ # return conditions for all entries between date1 and date2 - 1 day
+ if @rate.is_a?(HourlyRate)
+ { date_column => date1..(date2 - 1),
+ user_id: @rate.user_id,
+ project_id: @rate.project_id
+ }
+ else
+ { date_column => date1..(date2 - 1),
+ cost_type_id: @rate.cost_type_id,
+ }
+ end
+ end
+
+ def find_entries(date1, date2 = nil)
+ if @rate.is_a?(HourlyRate)
+ TimeEntry.includes(:rate).where(conditions_between(date1, date2))
+ else
+ CostEntry.includes(:rate).where(conditions_between(date1, date2))
+ end
+ end
+
+ def update_entries(entries, rate = @rate)
+ # This methods updates the given array of time or cost entries with the given rate
+ entries = [entries] unless entries.is_a?(Array)
+ ActiveRecord::Base.cache do
+ entries.each do |entry|
+ entry.update_costs!(rate)
+ end
+ end
+ end
+
+ def count_rates(date1, date2 = nil)
+ (@rate.class).where(conditions_between(date1, date2, :valid_from)).count
+ end
+
+ def orphaned_child_entries(date1, date2 = nil)
+ # This method returns all entries in child projects without an explicit
+ # rate or with a rate id of rate_id between date1 and date2
+ # i.e. the ones with an assigned default rate or without a rate
+ return [] unless @rate.is_a?(HourlyRate)
+
+ (date1, date2) = order_dates(date1, date2)
+
+ # This gets an array of all the ids of the DefaultHourlyRates
+ default_rates = DefaultHourlyRate.pluck(:id)
+
+ if date1.nil? || date2.nil?
+ # we have only one date, query >=
+ conditions = [
+ 'user_id = ? AND project_id IN (?) AND (rate_id IN (?) OR rate_id IS NULL) AND spent_on >= ?',
+ @rate.user_id, @rate.project.descendants.to_a, default_rates, date1 || date2
+ ]
+ else
+ # we have two dates, query between
+ conditions = [
+ 'user_id = ? AND project_id IN (?) AND (rate_id IN (?) OR rate_id IS NULL) AND spent_on BETWEEN ? AND ?',
+ @rate.user_id, @rate.project.descendants.to_a, default_rates, date1, date2
+ ]
+ end
+
+ TimeEntry.includes(:rate).where(conditions)
+ end
+
+ def child_entries(date1, date2 = nil)
+ # This method returns all entries in child projects without an explicit
+ # rate or with a rate id of rate_id between date1 and date2
+ # i.e. the ones with an assigned default rate or without a rate
+ return [] unless @rate.is_a?(HourlyRate)
+
+ (date1, date2) = order_dates(date1, date2)
+
+ if date1.nil? || date2.nil?
+ # we have only one date, query >=
+ conditions = [
+ 'user_id = ? AND project_id IN (?) AND rate_id = ? AND spent_on >= ?',
+ @rate.user_id, @rate.project.descendants.to_a, @rate.id, date1 || date2
+ ]
+ else
+ # we have two dates, query between
+ conditions = [
+ 'user_id = ? AND project_id IN (?) AND rate_id = ? AND spent_on BETWEEN ? AND ?',
+ @rate.user_id, @rate.project.descendants.to_a, @rate.id, date1, date2
+ ]
+ end
+
+ TimeEntry.includes(:rate).where(conditions)
+ end
+ end
+
+ def after_create(rate)
+ o = Methods.new(rate)
+
+ next_rate = rate.next
+ # get entries from the current project
+ entries = o.find_entries(rate.valid_from, (next_rate.valid_from if next_rate))
+
+ # and entries from subprojects that need updating (only applies to hourly_rates)
+ entries += o.orphaned_child_entries(rate.valid_from, (next_rate.valid_from if next_rate))
+
+ o.update_entries(entries)
+ end
+
+ def after_update(rate)
+ o = Methods.new(rate)
+
+ unless rate.valid_from_changed?
+ # We have not moved a rate, maybe just changed the rate value
+
+ return unless rate.rate_changed?
+ # Only the rate value was changed so just update the currently assigned entries
+ return after_create(rate)
+ end
+
+ # We have definitely moved the rate
+ if o.count_rates(rate.valid_from_was, rate.valid_from) > 0
+ # We have passed the boundary of another rate
+ # We do essantially the same as deleting the old rate and adding a new one
+
+ # So first assign all entries from the old a new rate
+ after_destroy(rate)
+
+ # Now update the newly assigned entries
+ after_create(rate)
+ else
+ # We have only moved the rate without passing other rates
+ # So we have to either assign some entries to our previous rate (if moved forwards)
+ # or assign some entries to self (if moved backwards)
+
+ # get entries from the current project
+ entries = o.find_entries(rate.valid_from_was, rate.valid_from)
+ # and entries from subprojects that need updating (only applies to hourly_rates)
+ entries += o.child_entries(rate.valid_from_was, rate.valid_from)
+
+ o.update_entries(entries, (rate.valid_from_was < rate.valid_from) ? rate.previous : rate)
+ end
+ end
+
+ def after_destroy(rate)
+ entry_class = rate.is_a?(HourlyRate) ? TimeEntry : CostEntry
+ entry_class.where(rate_id: rate.id).each(&:update_costs!)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/models/variable_cost_object.rb b/vendored-plugins/openproject-costs/app/models/variable_cost_object.rb
new file mode 100644
index 0000000000..78ae169da7
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/models/variable_cost_object.rb
@@ -0,0 +1,152 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class VariableCostObject < CostObject
+ has_many :material_budget_items, -> {
+ includes(:cost_type).order('material_budget_items.id ASC')
+ }, foreign_key: 'cost_object_id',
+ dependent: :destroy
+ has_many :labor_budget_items, -> {
+ includes(:user).order('labor_budget_items.id ASC')
+ }, foreign_key: 'cost_object_id',
+ dependent: :destroy
+
+ validates_associated :material_budget_items
+ validates_associated :labor_budget_items
+
+ after_update :save_material_budget_items
+ after_update :save_labor_budget_items
+
+ # override acts_as_journalized method
+ def activity_type
+ self.class.superclass.plural_name
+ end
+
+ def copy_from(arg)
+ cost_object = (arg.is_a?(VariableCostObject) ? arg : self.class.find(arg))
+ attrs = cost_object.attributes.dup
+ super(attrs)
+ self.labor_budget_items = cost_object.labor_budget_items.map(&:dup)
+ self.material_budget_items = cost_object.material_budget_items.map(&:dup)
+ end
+
+ # Label of the current cost_object type for display in GUI.
+ def type_label
+ l(:label_variable_cost_object)
+ end
+
+ def material_budget
+ @material_budget ||= material_budget_items.visible_costs.inject(BigDecimal.new('0.0000')) { |sum, i| sum += i.costs }
+ end
+
+ def labor_budget
+ @labor_budget ||= labor_budget_items.visible_costs.inject(BigDecimal.new('0.0000')) { |sum, i| sum += i.costs }
+ end
+
+ def spent
+ spent_material + spent_labor
+ end
+
+ def spent_material
+ @spent_material ||= begin
+ if cost_entries.blank?
+ BigDecimal.new('0.0000')
+ else
+ cost_entries.visible_costs(User.current, project).sum("CASE
+ WHEN #{CostEntry.table_name}.overridden_costs IS NULL THEN
+ #{CostEntry.table_name}.costs
+ ELSE
+ #{CostEntry.table_name}.overridden_costs END").to_d
+ end
+ end
+ end
+
+ def spent_labor
+ @spent_labor ||= begin
+ if time_entries.blank?
+ BigDecimal.new('0.0000')
+ else
+ time_entries.visible_costs(User.current, project).sum("CASE
+ WHEN #{TimeEntry.table_name}.overridden_costs IS NULL THEN
+ #{TimeEntry.table_name}.costs
+ ELSE
+ #{TimeEntry.table_name}.overridden_costs END").to_d
+ end
+ end
+ end
+
+ def new_material_budget_item_attributes=(material_budget_item_attributes)
+ material_budget_item_attributes.each do |_index, attributes|
+ material_budget_items.build(attributes) if attributes[:units].to_i > 0
+ end
+ end
+
+ def existing_material_budget_item_attributes=(material_budget_item_attributes)
+ material_budget_items.reject(&:new_record?).each do |material_budget_item|
+ attributes = material_budget_item_attributes[material_budget_item.id.to_s]
+
+ if User.current.allowed_to? :edit_cost_objects, material_budget_item.cost_object.project
+ if attributes && attributes[:units].to_i > 0
+ attributes[:budget] = Rate.clean_currency(attributes[:budget])
+ material_budget_item.attributes = attributes
+ else
+ material_budget_items.delete(material_budget_item)
+ end
+ end
+ end
+ end
+
+ def save_material_budget_items
+ material_budget_items.each do |material_budget_item|
+ material_budget_item.save(validate: false)
+ end
+ end
+
+ def new_labor_budget_item_attributes=(labor_budget_item_attributes)
+ labor_budget_item_attributes.each do |_index, attributes|
+ if attributes[:hours].to_i > 0 &&
+ attributes[:user_id].to_i > 0 &&
+ project.possible_assignees.map(&:id).include?(attributes[:user_id].to_i)
+
+ item = labor_budget_items.build(attributes)
+ item.cost_object = self # to please the labor_budget_item validation
+ end
+ end
+ end
+
+ def existing_labor_budget_item_attributes=(labor_budget_item_attributes)
+ labor_budget_items.reject(&:new_record?).each do |labor_budget_item|
+ attributes = labor_budget_item_attributes[labor_budget_item.id.to_s]
+ if User.current.allowed_to? :edit_cost_objects, labor_budget_item.cost_object.project
+ if attributes && attributes[:hours].to_i > 0 && attributes[:user_id].to_i > 0 && project.possible_assignees.map(&:id).include?(attributes[:user_id].to_i)
+ attributes[:budget] = Rate.clean_currency(attributes[:budget])
+ labor_budget_item.attributes = attributes
+ else
+ labor_budget_items.delete(labor_budget_item)
+ end
+ end
+ end
+ end
+
+ def save_labor_budget_items
+ labor_budget_items.each do |labor_budget_item|
+ labor_budget_item.save(validate: false)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_costs.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_costs.html.erb
new file mode 100644
index 0000000000..1ccc5b0d7d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_costs.html.erb
@@ -0,0 +1,21 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= costs > 0 ? costs : "-" %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_edit.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_edit.html.erb
new file mode 100644
index 0000000000..645457029f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_edit.html.erb
@@ -0,0 +1,33 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= labelled_tabular_form_for @cost_object,
+ :as => :cost_object,
+ :url => cost_object_path(@cost_object),
+ :html => {:multipart => true,
+ :id => 'cost_object_form',
+ :class => 'form'} do |f| %>
+ <%= error_messages_for 'cost_object' %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= l(:label_attachment_plural )%>
+ <%= render :partial => 'attachments/form' %>
+
+ <%= styled_button_tag l(:button_submit), class: '-with-icon icon-checkmark -highlight', id: 'budget-table--submit-button'%>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_form.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_form.html.erb
new file mode 100644
index 0000000000..808557773c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_form.html.erb
@@ -0,0 +1,199 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= f.hidden_field :kind %>
+
+
+ <%= f.text_field :subject, required: true %>
+
+
+ <%= f.text_area :description, rows: (@cost_object.description.blank? ? 10 : [[10, @cost_object.description.length / 50].max, 100].min), cols: 60 %>
+
+
+ <%= f.text_field :fixed_date, container_class: '-xslim' %>
+ <%= calendar_for :cost_object_fixed_date %>
+
+
+<% if @cost_object.kind == "VariableCostObject" -%>
+
+
+
+ <%= VariableCostObject.human_attribute_name(:material_budget) %>
+
+
+
+
+
+
+
+ <% if User.current.allowed_to?(:view_cost_rates, @project)%>
+
+ <%end%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% if User.current.allowed_to?(:view_cost_rates, @project)%>
+
+
+
+ <%end%>
+
+
+
+
+
+ <%- @cost_object.material_budget_items.each_with_index do |material_budget_item, index| -%>
+ <%= render :partial => 'material_budget_item', :object => material_budget_item, :locals => {:index => index} %>
+ <%- end -%>
+
+
+
+
+
+
+ <%= link_to_function l(:button_add_budget_item), "materialBudgetItemsForm.add()", {class: "button -with-icon icon-context icon-add"} %>
+
+
+
+
+ <%= VariableCostObject.human_attribute_name(:labor_budget) %>
+
+
+
+
+
+
+
+ <% if User.current.allowed_to?(:view_cost_rates, @project)%>
+
+ <%end%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% if User.current.allowed_to?(:view_cost_rates, @project)%>
+
+
+
+ <%end%>
+
+
+
+
+ <%- @cost_object.labor_budget_items.each_with_index do |labor_budget_item, index| -%>
+ <%= render :partial => 'labor_budget_item', :object => labor_budget_item, :locals => {:index => index} %>
+ <%- end -%>
+
+
+
+
+
+
+ <%= link_to_function l(:button_add_budget_item), "laborBudgetItemsForm.add()", {class: "button -with-icon icon-context icon-add"} %>
+
+
+
+<%- end %>
+
+
+
+<% if @cost_object.new_record? %>
+<%=l(:label_attachment_plural)%> <%= render :partial => 'attachments/form' %>
+<% end %>
+
+<%= wikitoolbar_for 'cost_object_description' %>
+
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_labor_budget_item.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_labor_budget_item.html.erb
new file mode 100644
index 0000000000..38be4a405d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_labor_budget_item.html.erb
@@ -0,0 +1,64 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%-
+ index ||= "INDEX"
+ new_or_existing = labor_budget_item.new_record? ? 'new' : 'existing'
+ id_or_index = labor_budget_item.new_record? ? index : labor_budget_item.id
+ prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][]"
+ id_prefix = "cost_object_#{new_or_existing}_labor_budget_item_attributes_#{id_or_index}"
+ name_prefix = "cost_object[#{new_or_existing}_labor_budget_item_attributes][#{id_or_index}]"
+ classes ||= ""
+
+ @labor_budget_item = labor_budget_item
+ error_messages = error_messages_for 'labor_budget_item'
+-%>
+
+<% unless error_messages.blank? %><%= error_messages %> <% end %>
+<%= fields_for prefix, labor_budget_item do |cost_form| %>
+
+
+ <%= LaborBudgetItem.human_attribute_name(:hours) %>
+ <%= cost_form.text_field :hours, :index => id_or_index, :size => 3 %>
+
+
+ <%= l(:label_user) %>
+ <%= cost_form.select :user_id, @project.possible_assignees.sort.map{|u| [u.name, u.id]}, {:prompt => true}, {:index => id_or_index, :class => 'form--select'}%>
+
+
+
+ " class="icon-context icon-edit" title="<%= l(:help_click_to_edit) %>">
+ <%= number_to_currency(labor_budget_item.calculated_costs(@cost_object.fixed_date, @cost_object.project_id)) if labor_budget_item.costs_visible_by?(User.current) %>
+
+ <%= update_page_tag do |page|
+ page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');"
+ page << "edit($('#{id_prefix}_costs'), '#{name_prefix}[budget]', '#{number_to_currency(labor_budget_item.budget)}');" if labor_budget_item.budget
+ end %>
+ <%= observe_field( "#{id_prefix}_user_id", :url => {:action => :update_labor_budget_item, :project_id => @project.id}, :with => "'user_id=' + encodeURIComponent(value) + '&hours=' + encodeURIComponent(document.getElementById('#{id_prefix}_hours').value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
+ <%= observe_field( "#{id_prefix}_hours", :frequency => 1, :url => {:action => :update_labor_budget_item, :project_id => @project.id}, :with => "'user_id=' + encodeURIComponent(document.getElementById('#{id_prefix}_user_id').value) + '&hours=' + encodeURIComponent(value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
+
+
+ <%= link_to_function icon_wrapper('icon-context icon-delete',t(:button_delete)), "deleteLaborBudgetItem('#{id_prefix}')", :class => 'no-decoration-on-hover', :title => t(:button_delete) %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_list.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_list.html.erb
new file mode 100644
index 0000000000..cf381bfb20
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_list.html.erb
@@ -0,0 +1,110 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= form_tag({}) do -%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= sort_header_tag("id", :caption => '#', :default_order => 'desc') %>
+ <%= sort_header_tag("subject", :caption => CostObject.human_attribute_name(:subject)) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% total_budget = BigDecimal.new("0"); labor_budget = BigDecimal.new("0"); material_budget = BigDecimal.new("0"); spent = BigDecimal.new("0") %>
+ <% cost_objects.each do |cost_object| %>
+
+ <%= link_to cost_object.id, cost_object_path(cost_object.id) %>
+ <%= content_tag(:td, link_to(h(cost_object.subject), cost_object_path(cost_object.id)), :class => 'subject') %>
+ <%= content_tag(:td, number_to_currency(cost_object.budget, :precision => 0), :class => 'currency') %>
+ <%= content_tag(:td, number_to_currency(cost_object.spent, :precision => 0), :class => 'currency') %>
+ <%= content_tag(:td, number_to_currency(cost_object.budget - cost_object.spent, :precision => 0), :class => 'currency') %>
+ <%= content_tag(:td, extended_progress_bar(cost_object.budget_ratio, :legend => "#{cost_object.budget_ratio}")) %>
+ <%-
+ total_budget += cost_object.budget
+ labor_budget += cost_object.labor_budget
+ material_budget += cost_object.material_budget
+ spent += cost_object.spent
+ -%>
+
+ <% end %>
+ <% if cost_objects.length > 0 %>
+
+
+
+ <%= number_to_currency( total_budget, :precision => 0) %>
+ <%= number_to_currency( spent, :precision => 0) %>
+ <%= number_to_currency( total_budget - spent, :precision => 0) %>
+
+
+ <% end %>
+
+
+
+
+
+
+<% end -%>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_material_budget_item.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_material_budget_item.html.erb
new file mode 100644
index 0000000000..da51cb56c4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_material_budget_item.html.erb
@@ -0,0 +1,71 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%-
+ index ||= "INDEX"
+ new_or_existing = material_budget_item.new_record? ? 'new' : 'existing'
+ id_or_index = material_budget_item.new_record? ? index : material_budget_item.id
+ prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][]"
+ id_prefix = "cost_object_#{new_or_existing}_material_budget_item_attributes_#{id_or_index}"
+ name_prefix = "cost_object[#{new_or_existing}_material_budget_item_attributes][#{id_or_index}]"
+ classes ||= ""
+
+ @material_budget_item = material_budget_item
+ error_messages = error_messages_for 'material_budget_item'
+-%>
+
+<% unless error_messages.blank? %><%= error_messages %> <% end %>
+<%= fields_for prefix, material_budget_item do |cost_form| %>
+
+
+
+
+ ">
+ <%=h material_budget_item.cost_type.unit_plural if material_budget_item.cost_type %>
+
+
+ <%= MaterialBudgetItem.human_attribute_name(:cost_type) %>
+ <%= cost_form.select :cost_type_id, cost_types_collection_for_select_options(material_budget_item.cost_type), {}, {:index => id_or_index, :class => 'form--select'} %>
+ <%= observe_field( "#{id_prefix}_cost_type_id", :url => {:action => :update_material_budget_item, :project_id => @project.id}, :with => "'cost_type_id=' + encodeURIComponent(value) + '&units=' + encodeURIComponent(document.getElementById('#{id_prefix}_units').value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
+ <%= observe_field( "#{id_prefix}_units", :frequency => 1, :url => {:action => :update_material_budget_item, :project_id => @project.id}, :with => "'cost_type_id=' + encodeURIComponent(document.getElementById('#{id_prefix}_cost_type_id').value) + '&units=' + encodeURIComponent(value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_object_fixed_date').value) + '&element_id=#{id_prefix}'") %>
+
+
+
+ " class="icon-context icon-edit" title="<%= l(:help_click_to_edit) %>">
+ <%= number_to_currency(material_budget_item.calculated_costs(@cost_object.fixed_date)) %>
+
+ <%= update_page_tag do |page|
+ page << "makeEditable('#{id_prefix}_costs', '#{name_prefix}[budget]');"
+ page << "edit($('#{id_prefix}_costs'), '#{name_prefix}[budget]', '#{number_to_currency(material_budget_item.budget)}');" if material_budget_item.budget
+ end %>
+
+
+ <%= link_to_function icon_wrapper('icon-context icon-delete',t(:button_delete)), "deleteMaterialBudgetItem('#{id_prefix}')", :class => 'no-decoration-on-hover', :title => t(:button_delete) %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/_show_variable_cost_object.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/_show_variable_cost_object.html.erb
new file mode 100644
index 0000000000..d49d225ad9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/_show_variable_cost_object.html.erb
@@ -0,0 +1,380 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= l(:caption_materials) %>
+
+
+
<%= VariableCostObject.human_attribute_name(:material_budget) %>
+
+
+
+
+
+
<%= l(:caption_material_costs) %>
+
+
+
+
+
+
+<%= l(:caption_labor) %>
+
+
+
<%= VariableCostObject.human_attribute_name(:labor_budget)%>
+
+
+
+
+
+
<%= l(:caption_labor_costs) %>
+
+
+
+
+
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/edit.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/edit.html.erb
new file mode 100644
index 0000000000..090ba33d25
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/edit.html.erb
@@ -0,0 +1,27 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title "#{l(:label_edit)} #{l(:label_cost_object_id, id: @cost_object.id)}: #{@cost_object.subject}" %>
+<%= toolbar title: l(:label_cost_object_id, id: @cost_object.id) %>
+
+<%= render :partial => 'edit' %>
+<%= javascript_tag "Form.Element.focus('cost_object_subject');" %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/index.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/index.html.erb
new file mode 100644
index 0000000000..61e5451712
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/index.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title(l(:label_cost_object_plural)) %>
+<%= toolbar title: l(:label_cost_object_plural) do %>
+ <% if authorize_for(:cost_objects, :new) %>
+
+
+ <%= I18n.t(:button_add_cost_object) %>
+
+ <% end %>
+<% end %>
+
+<% if @cost_objects.empty? %>
+<%= l(:label_no_data) %>
+<% else %>
+<%= render :partial => 'list', :locals => {:cost_objects => @cost_objects} %>
+<%= pagination_links_full @cost_objects %>
+<% end %>
+
+
+<%= l(:label_export_to) %>
+
+<%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %>
+
+
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/new.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/new.html.erb
new file mode 100644
index 0000000000..c259587364
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/new.html.erb
@@ -0,0 +1,37 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title(l(:label_cost_object_new)) %>
+<%= toolbar title: l(:label_cost_object_new) %>
+
+<%= labelled_tabular_form_for @cost_object,
+ :url => projects_cost_objects_path(@project),
+ :as => :cost_object,
+ :html => {:multipart => true, :id => 'cost_object_form'} do |f| %>
+ <%= error_messages_for 'cost_object' %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= styled_button_tag l(:button_create), class: '-with-icon icon-checkmark' %>
+ <%= styled_button_tag l(:button_create_and_continue), :name => 'continue',
+ class: 'button -highlight'%>
+
+ <%= javascript_tag "Form.Element.focus('cost_object_subject');" %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_objects/show.html.erb b/vendored-plugins/openproject-costs/app/views/cost_objects/show.html.erb
new file mode 100644
index 0000000000..e79ce9ee61
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_objects/show.html.erb
@@ -0,0 +1,87 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title "#{l(:label_cost_object_id, id: @cost_object.id)}: #{@cost_object.subject}" %>
+<%= toolbar title: l(:label_cost_object_id, id: @cost_object.id) do %>
+ <% if authorize_for(:cost_objects, :edit) %>
+
+ <%= link_to({ controller: 'cost_objects', action: 'edit', id: @cost_object }, class: 'button', accesskey: accesskey(:edit)) do %>
+ <%= l(:button_update) %>
+ <% end %>
+
+ <% end %>
+ <% if authorize_for(:cost_objects, :copy) %>
+
+ <%= link_to({ controller: 'cost_objects', action: 'copy', id: @cost_object }, class: 'button') do %>
+ <%= l(:button_copy) %>
+ <% end %>
+
+ <% end %>
+ <% if authorize_for(:cost_objects, :copy) %>
+
+ <%= link_to({ controller: 'cost_objects', action: 'destroy', id: @cost_object }, class: 'button', method: :delete, data: { confirm: l(:text_are_you_sure)}) do %>
+ <%= l(:button_delete) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
+
<%=h @cost_object.subject %>
+
+ <%= authoring @cost_object.created_on, @cost_object.author %>.
+ <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @cost_object.updated_on)) + '.' if @cost_object.created_on != @cost_object.updated_on %>
+
+
+
+
+ <%= CostObject.human_attribute_name(:type) %>:
+ <%= @cost_object.type_label %>
+
+
+ <%= CostObject.human_attribute_name(:fixed_date) %>:
+ <%= format_date(@cost_object.fixed_date) %>
+
+
+ <%= CostObject.human_attribute_name(:budget_ratio) %>:
+ <%= extended_progress_bar(@cost_object.budget_ratio, :width => '80px', :legend => @cost_object.budget_ratio) %>
+
+
+
+
<%= CostObject.human_attribute_name(:description) %>
+
+ <%= format_text @cost_object, :description, :attachments => @cost_object.attachments %>
+
+
+ <%= link_to_attachments @cost_object %>
+
+ <%= render :partial => "show_variable_cost_object" %>
+
+
+
+
+<% if authorize_for('cost_objects', 'edit') %>
+
+
<%= l(:button_update) %>
+ <%= render :partial => 'edit' %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_types/_list.html.erb b/vendored-plugins/openproject-costs/app/views/cost_types/_list.html.erb
new file mode 100644
index 0000000000..ab857fbd9b
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_types/_list.html.erb
@@ -0,0 +1,122 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% cost_types = @cost_types.reject(&:deleted_at) -%>
+
+<% if cost_types.empty? %>
+
+
+
+
+ <%= l(:label_nothing_display) %>
+
+
+
<%= l(:label_no_data) %>
+
+
+
+<% else %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= sort_header_tag "name", :caption => CostType.model_name.human %>
+ <%= sort_header_tag "unit", :caption => CostType.human_attribute_name(:unit) %>
+ <%= sort_header_tag "unit_plural", :caption => CostType.human_attribute_name(:unit_plural) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% cost_types.each do |cost_type| %>
+
+ <%= content_tag :td, link_to(cost_type.name, {:controller => '/cost_types', :action => 'edit', :id => cost_type}) %>
+ <%= content_tag :td, cost_type.unit %>
+ <%= content_tag :td, cost_type.unit_plural %>
+ <%= content_tag :td, to_currency_with_empty(cost_type.rate_at(@fixed_date)), :class => "currency", :id => "cost_type_#{cost_type.id}_rate"%>
+
+ <%= form_for cost_type, :url => { :controller => '/cost_types', :action => 'set_rate', :id => cost_type } do |f| %>
+
+ "><%= l(:caption_set_rate) %>
+ <%= f.text_field :rate, :value => "", :name => :rate, :size => 7, :id => "rate_field_#{cost_type.id}" %>
+
+ <%= Setting.plugin_openproject_costs['costs_currency'] %>
+
+
+ <%= icon_wrapper('icon icon-save', I18n.t(:caption_save_rate)) %>
+
+
+ <% end %>
+
+ <%= content_tag :td, cost_type.is_default? ? icon_wrapper('icon icon-checkmark', I18n.t(:general_text_Yes)) : "" %>
+
+ <%= form_for cost_type, url: cost_type_path(cost_type),
+ method: :delete,
+ html: { id: "delete_cost_type_#{cost_type.id}",
+ class: 'delete_cost_type' } do |f| %>
+ <%= icon_wrapper('icon icon-locked', I18n.t(:button_lock)) %>
+ <% end %>
+
+
+
+
+ <% end %>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_types/_list_deleted.html.erb b/vendored-plugins/openproject-costs/app/views/cost_types/_list_deleted.html.erb
new file mode 100644
index 0000000000..608ed1d72c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_types/_list_deleted.html.erb
@@ -0,0 +1,96 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% cost_types = @cost_types.select(&:deleted_at) -%>
+
+<% if cost_types.empty? %>
+
+
+
+
+ <%= l(:label_nothing_display) %>
+
+
+
<%= l(:label_no_data) %>
+
+
+
+<% else %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= sort_header_tag("name", :caption => CostType.model_name.human) %>
+ <%= sort_header_tag("unit", :caption => CostType.human_attribute_name(:unit)) %>
+ <%= sort_header_tag("unit_plural", :caption => CostType.human_attribute_name(:unit_plural)) %>
+
+
+
+
+
+
+
+
+
+
+ <% cost_types.each do |cost_type| %>
+
+ <%= content_tag :td, cost_type.name %>
+ <%= content_tag :td, cost_type.unit %>
+ <%= content_tag :td, cost_type.unit_plural %>
+ <%= content_tag :td, to_currency_with_empty(cost_type.rate_at(@fixed_date)), :class => "currency" %>
+ <%= content_tag :td, cost_type.deleted_at.to_date %>
+
+ <%= form_for cost_type, url: restore_cost_type_path(cost_type),
+ method: :patch,
+ html: { id: "restore_cost_type_#{cost_type.id}",
+ class: 'restore_cost_type' } do |f| %>
+ <%= icon_wrapper('icon icon-unlocked', I18n.t(:button_unlock)) %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_types/_rate.html.erb b/vendored-plugins/openproject-costs/app/views/cost_types/_rate.html.erb
new file mode 100644
index 0000000000..e085292ac3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_types/_rate.html.erb
@@ -0,0 +1,57 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%-
+ index ||= "INDEX"
+ new_or_existing = rate.new_record? ? 'new' : 'existing'
+ id_or_index = rate.new_record? ? index : rate.id
+ prefix = "cost_type[#{new_or_existing}_rate_attributes][]"
+ id_prefix = "cost_type_#{new_or_existing}_rate_attributes_#{id_or_index}"
+ name_prefix = "cost_type[#{new_or_existing}_rate_attributes][#{id_or_index}]"
+ classes ||= ""
+
+ @rate = rate
+ error_messages = error_messages_for 'rate'
+-%>
+
+
+<% unless error_messages.blank? %>
+ <%= error_messages %>
+<% end %>
+<%= fields_for prefix, rate do |rate_form| %>
+
+
+ "><%= Rate.human_attribute_name(:valid_from) %>
+ <%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %>
+
+
+
+ "><%= Rate.model_name.human %>
+ <%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %>
+
+ <%= Setting.plugin_openproject_costs['costs_currency'] %>
+
+
+
+
+ <%= icon_wrapper('icon-context icon-delete', I18n.t(:button_delete)) %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_types/edit.html.erb b/vendored-plugins/openproject-costs/app/views/cost_types/edit.html.erb
new file mode 100644
index 0000000000..ffdadf9c76
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_types/edit.html.erb
@@ -0,0 +1,108 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title l(:label_cost_type_specific, id: @cost_type.id, name: @cost_type.name) %>
+
+<%= toolbar title: CostType.model_name.human %>
+
+<%= labelled_tabular_form_for @cost_type do |f| %>
+ <%= error_messages_for 'cost_type' %>
+ <%= back_url_hidden_field_tag %>
+
+
+ <%= f.text_field :name, required: true %>
+
+
+ <%= f.text_field :unit, required: true %>
+
+
+ <%= f.text_field :unit_plural, required: true %>
+
+
+ <%= f.check_box :default %>
+
+
+ <%= l :caption_rate_history %>
+ <%= javascript_tag do -%>
+ RatesForm = new Subform('<%= escape_javascript(render(:partial => "rate", :object => CostRate.new )) %>',<%= @cost_type.rates.length %>,'rates_body');
+ <% end -%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% @cost_type.rates.sort do |a,b|
+ case
+ when !a.valid? && !b.valid?
+ 0
+ when !a.valid?
+ -1
+ when !b.valid?
+ 1
+ else
+ b.valid_from <=> a.valid_from
+ end
+ end.each_with_index do |rate, index| %>
+ <%= render :partial => 'rate', :object => rate, :locals => {:index => index} %>
+ <%- end -%>
+
+
+
+
+
+
+ "><%= l(:description_date_for_new_rate) %>
+ <%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {:class => "button icon icon-add"} %>
+
+
+ <%= styled_button_tag l(:button_save), class: '-with-icon icon-checkmark' %>
+<% end %>
+
+
+<%= javascript_tag "Form.Element.focus('cost_type_name');" %>
diff --git a/vendored-plugins/openproject-costs/app/views/cost_types/index.html.erb b/vendored-plugins/openproject-costs/app/views/cost_types/index.html.erb
new file mode 100644
index 0000000000..1a6cc7e47c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/cost_types/index.html.erb
@@ -0,0 +1,67 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title l(:label_cost_type_plural) %>
+<%= toolbar title: CostType.model_name.human(count: 2) do %>
+
+ <%= link_to new_cost_type_path, class: 'button -alt-highlight' do%>
+ <%= l(:button_add_cost_type) %>
+ <% end %>
+
+<% end %>
+
+<%= styled_form_tag(cost_types_path, { :method => :get, :id => 'query_form' }) do %>
+
+ <%= l(:label_filter_plural) %>
+
+ <%= styled_label_tag :fixed_date, l(:label_fixed_date) %>
+ <%= styled_text_field_tag :fixed_date, @fixed_date %>
+ <%= calendar_for :fixed_date %>
+
+
+ <%= styled_label_tag :include_deleted, l(:caption_show_locked) %>
+ <%= styled_check_box_tag :include_deleted, "1", @include_deleted, :autocomplete => "off" %>
+
+
+
+<%= link_to_remote l(:button_apply),
+ { :update => "content",
+ :with => "Form.serialize('query_form')",
+ :method => :get
+ }, :class => 'button -with-icon icon-checkmark' %>
+
+<%= link_to_remote l(:button_clear),
+ { :url => { :clear_filter => true },
+ :method => :get,
+ :update => "content",
+ }, :class => 'button icon-context icon-undo' %>
+<% end %>
+
+
+
<%= l(:notice_successful_update) %>
+
+<%= render :partial => 'list' %>
+
+<% if @include_deleted %>
+ <%= l(:label_locked_cost_types) %>
+ <%= render :partial => 'list_deleted' %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/costlog/_cost_type_unit_plural.html.erb b/vendored-plugins/openproject-costs/app/views/costlog/_cost_type_unit_plural.html.erb
new file mode 100644
index 0000000000..6c8fc08501
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/costlog/_cost_type_unit_plural.html.erb
@@ -0,0 +1,21 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= h(@cost_type.unit_plural) if @cost_type %>
diff --git a/vendored-plugins/openproject-costs/app/views/costlog/_date_range.html.erb b/vendored-plugins/openproject-costs/app/views/costlog/_date_range.html.erb
new file mode 100644
index 0000000000..c63bd9770d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/costlog/_date_range.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+ <%= l(:label_date_range) %>
+
+ <%= styled_radio_button_tag 'period_type', '1', !@free_period %>
+
+
+ <%= styled_select_tag 'period', options_for_period_select(params[:period]),
+ :onchange => 'this.form.onsubmit();',
+ :onfocus => '$("period_type_1").checked = true;' %>
+
+
+ <%= styled_radio_button_tag 'period_type', '2', @free_period %>
+
+
+
<%= l(:label_date_from) %>
+
+ <%= styled_label_tag :from, l(:description_date_range_list) %>
+ <%= styled_text_field_tag :from, @from %>
+ <%= calendar_for :from %>
+
+
<%= l(:label_date_to) %>
+
+ <%= styled_label_tag :to, l(:description_date_range_interval) %>
+ <%= styled_text_field_tag :to, @to %>
+ <%= calendar_for :to %>
+
+
+ <%= styled_button_tag l(:button_apply), class: '-with-icon icon-checkmark' %>
+
diff --git a/vendored-plugins/openproject-costs/app/views/costlog/_list.html.erb b/vendored-plugins/openproject-costs/app/views/costlog/_list.html.erb
new file mode 100644
index 0000000000..8453d263dc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/costlog/_list.html.erb
@@ -0,0 +1,93 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
+ <%= sort_header_tag('user', :caption => Member.model_name.human) %>
+ <%= sort_header_tag('project', :caption => Project.model_name.human )%>
+ <%= sort_header_tag('work_package', :caption => WorkPackage.model_name.human, :default_order => 'desc') %>
+
+
+
+ <%= sort_header_tag('units', :caption => l(:label_units)) %>
+ <%= sort_header_tag('costs', :caption => l(:label_overall_costs)) %>
+
+
+
+
+ <% entries.each do |entry| -%>
+
+ <%= format_date(entry.spent_on) %>
+ <%=h entry.user %>
+ <%=h entry.project %>
+
+ <% if entry.work_package -%>
+ <%= link_to_work_package entry.work_package -%>
+ <% end -%>
+
+
+
+ <%= pluralize(entry.units, entry.cost_type.unit, entry.cost_type.unit_plural) %>
+
+ <% if entry.costs_visible_by?(User.current) -%>
+ <%= number_to_currency entry.real_costs %>
+ <%- else -%>-<% end -%>
+
+
+ <% if entry.editable_by?(User.current) -%>
+ <%= link_to icon_wrapper('icon-context icon-edit',t(:button_edit)),
+ {:controller => '/costlog', :action => 'edit', :id => entry, :project_id => nil},
+ :class => 'no-decoration-on-hover',
+ :title => l(:button_edit) %>
+ <%= link_to icon_wrapper('icon-context icon-delete',t(:button_delete)),
+ {:controller => '/costlog', :action => 'destroy', :id => entry, :project_id => nil},
+ :confirm => l(:text_are_you_sure),
+ :method => :delete,
+ :class => 'no-decoration-on-hover',
+ :title => l(:button_delete) %>
+ <% end -%>
+
+
+ <% end -%>
+
+
+
+
+
diff --git a/vendored-plugins/openproject-costs/app/views/costlog/edit.html.erb b/vendored-plugins/openproject-costs/app/views/costlog/edit.html.erb
new file mode 100644
index 0000000000..6dfe7220d8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/costlog/edit.html.erb
@@ -0,0 +1,104 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+<%= l(:label_log_costs) %>
+
+<% url = @cost_entry.new_record? ?
+ { :action => 'create', :project_id => @project.id } :
+ { :action => 'update', :id => @cost_entry } %>
+
+<% method = @cost_entry.new_record? ?
+ :post :
+ :put %>
+
+<%= labelled_tabular_form_for @cost_entry, :url => url, :html => { :method => method } do |f| %>
+ <%= error_messages_for 'cost_entry' %>
+ <%= back_url_hidden_field_tag %>
+
+
+
+
+ <%= f.text_field :spent_on, :size => 10, :required => true %>
+ <%= calendar_for('cost_entry_spent_on') %>
+
+ <% if User.current.allowed_to? :log_costs, @project %>
+
+ <%= f.select :user_id, user_collection_for_select_options, :required => true %>
+
+ <% else %>
+ <%= f.hidden_field :user_id, :value => User.current.id %>
+ <% end %>
+
+ <%= f.select :cost_type_id, cost_types_collection_for_select_options, :required => true %>
+
+
+ <%= f.label :units, class: "form--label -required" %>
+ <% suffix = @cost_entry.cost_type.nil? ? '' : (@cost_entry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural) %>
+
+ <%= f.text_field :units, size: 6, required: true, no_label: true %>
+ <% if @cost_entry.cost_type %>
+
+ <%= h(@cost_entry.units == 1 ? @cost_entry.cost_type.unit : @cost_entry.cost_type.unit_plural) %>
+
+ <% end %>
+
+
+
+ <%= observe_field( "cost_entry_cost_type_id", :url => {:controller => :cost_objects, :action => :update_material_budget_item, :project_id => @cost_entry.project}, :with => "'cost_type_id=' + encodeURIComponent(value) + '&units=' + encodeURIComponent(document.getElementById('cost_entry_units').value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_entry_spent_on').value) + '&element_id=cost_entry'") %>
+ <%= observe_field( "cost_entry_units", :frequency => 1, :url => {:controller => :cost_objects, :action => :update_material_budget_item, :project_id => @cost_entry.project}, :with => "'cost_type_id=' + encodeURIComponent(document.getElementById('cost_entry_cost_type_id').value) + '&units=' + encodeURIComponent(value) + '&fixed_date=' + encodeURIComponent(document.getElementById('cost_entry_spent_on').value) + '&element_id=cost_entry'") %>
+ <%= observe_field( "cost_entry_spent_on", :frequency => 1, :url => {:controller => :cost_objects, :action => :update_material_budget_item, :project_id => @cost_entry.project}, :with => "'cost_type_id=' + encodeURIComponent(document.getElementById('cost_entry_cost_type_id').value) + '&units=' + encodeURIComponent(document.getElementById('cost_entry_units').value) + '&fixed_date=' + encodeURIComponent(value) + '&element_id=cost_entry'") %>
+
+
+
+
+
+ <%= f.text_field :comments, size: 100 %>
+
+
+
+ <%= styled_button_tag l(:button_save), class: '-with-icon icon-checkmark' %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/costlog/index.html.erb b/vendored-plugins/openproject-costs/app/views/costlog/index.html.erb
new file mode 100644
index 0000000000..648990b03b
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/costlog/index.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<%= render_costlog_breadcrumb %>
+
+<%= toolbar title: WorkPackage.human_attribute_name(:spent_costs) do %>
+ <% if authorize_for(:costlog, :new) %>
+ <%= link_to({:controller => '/costlog', :action => 'new', :work_package_id => @work_package }, class: 'button') do %>
+ <%= l(:button_log_costs) %>
+ <% end %>
+ <% end %>
+<% end %>
+
+<%= form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
+<%= hidden_field_tag('project_id', params[:project_id]) if @project %>
+<%= hidden_field_tag 'work_package_id', params[:work_package_id] if @work_package %>
+<%= hidden_field_tag 'cost_type_id', params[:cost_type_id] if @cost_type %>
+<%= render :partial => 'date_range' %>
+<% end %>
+
+<% unless @entries.empty? %>
+<%= render :partial => 'list', :locals => { :entries => @entries }%>
+<%= pagination_links_full @entries %>
+<% end %>
+
+<% html_title WorkPackage.human_attribute_name(:spent_costs), l(:label_details) %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:work_package_id => @work_package, :format => 'atom', :key => User.current.rss_key}, :title => WorkPackage.human_attribute_name(:spent_costs)) %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_activity_index_head.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_activity_index_head.html.erb
new file mode 100644
index 0000000000..54be7e9904
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_activity_index_head.html.erb
@@ -0,0 +1,21 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= stylesheet_link_tag 'costs/costs.css' %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_colgroup.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_colgroup.html.erb
new file mode 100644
index 0000000000..4e32add2e2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_colgroup.html.erb
@@ -0,0 +1,2 @@
+
+
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_header.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_header.html.erb
new file mode 100644
index 0000000000..f52107ca71
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_header.html.erb
@@ -0,0 +1,33 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% if not project.nil? and project.module_enabled? :costs_module %>
+ <% if User.current.allowed_to?(:view_hourly_rates, project) %>
+
+
+
+ <% end %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_row.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_row.html.erb
new file mode 100644
index 0000000000..5ee806e6ad
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_projects_settings_members_table_row.html.erb
@@ -0,0 +1,44 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+<% if not project.nil? and project.module_enabled? :costs_module and User.current.allowed_to?(:view_hourly_rates, project) %>
+<% unless member.user %>
+
+
+<% else %>
+<%
+ allow_view = User.current.allowed_to?(:view_hourly_rates, project, :for => member.user)
+ allow_edit = User.current.allowed_to?(:edit_hourly_rates, project, :for => member.user)
+-%>
+
+<%- if allow_view -%>
+ <% rate = member.user.current_rate(project) -%>
+
+ <%= link_to number_to_currency( rate ? rate.rate : 0.0), :controller => '/hourly_rates', :action => (allow_edit ? 'edit' : 'show'), :id => member.user, :project_id => project %>
+
+<%- else -%>
+
+<%- end -%>
+
+
+
+<% end %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb
new file mode 100644
index 0000000000..54be7e9904
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_overview_attributes.html.erb
@@ -0,0 +1,21 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= stylesheet_link_tag 'costs/costs.css' %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_show_action_menu.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_show_action_menu.html.erb
new file mode 100644
index 0000000000..9ac51a021f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_package_show_action_menu.html.erb
@@ -0,0 +1,24 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% if @project && @project.module_enabled?(:costs_module) %>
+
+ <%= li_unless_nil(link_to_if_authorized l(:button_log_costs), {:controller => '/costlog', :action => 'new', :work_package_id => work_package}, :class => 'icon-context icon-projects') %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_bulk_edit_details_bottom.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_bulk_edit_details_bottom.html.erb
new file mode 100644
index 0000000000..92a84965f0
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_bulk_edit_details_bottom.html.erb
@@ -0,0 +1,28 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% if @project && @project.module_enabled?(:costs_module) %>
+ <% myselect = select_tag('cost_object_id',
+ content_tag('option', l(:label_no_change_option), :value => '') +
+ content_tag('option', l(:label_none), :value => 'none') +
+ options_from_collection_for_select(CostObject.where(project_id: @project.id).order('subject ASC'), :id, :subject))
+ %>
+ <%= content_tag :p, (content_tag "label", CostObject.model_name.human, :for => 'cost_object_id') + myselect %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_move_bottom.html.erb b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_move_bottom.html.erb
new file mode 100644
index 0000000000..8f4a704da3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hooks/costs/_view_work_packages_move_bottom.html.erb
@@ -0,0 +1,28 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% if @project && @project.module_enabled?(:costs_module) %>
+
+ <%= styled_label_tag :cost_object_id, CostObject.model_name.human %>
+ <%= styled_select_tag('cost_object_id', (@target_project == @project ? content_tag('option', l(:label_no_change_option), :value => '') : "") +
+ content_tag('option', l(:label_none), :value => 'none') +
+ options_from_collection_for_select(@target_project.cost_objects, :id, :subject)) %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/_list.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list.html.erb
new file mode 100644
index 0000000000..03fece8831
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list.html.erb
@@ -0,0 +1,23 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% @rates.each_pair do |project, rates| %>
+ <%= render :partial => 'hourly_rates/list_project', :locals => { rates: rates, project: project, all_rates: @rates } %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_default.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_default.html.erb
new file mode 100644
index 0000000000..2fc3025f79
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_default.html.erb
@@ -0,0 +1,91 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+ <%= link_to l(:button_update), edit_hourly_rate_path(@user), :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
+
+<%= User.human_attribute_name(:default_rates) %>
+<% if @rates_default.blank? %>
+
+
+
+
+ <%= l(:label_nothing_display) %>
+
+
+
<%= l(:label_no_data) %>
+
+
+
+<% else %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% current_rate = @user.current_default_rate() %>
+ <%- @rates_default.each do |rate| -%>
+
+ <%= rate.valid_from %>
+ <%= number_to_currency(rate.rate) %>
+ <%= rate == current_rate ? icon_wrapper('icon-context icon-checkmark',I18n.t(:general_text_Yes)) : "" %>
+
+ <%- end -%>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_project.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_project.html.erb
new file mode 100644
index 0000000000..9e87142e83
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/_list_project.html.erb
@@ -0,0 +1,103 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%
+ rates = @rates unless rates
+ project = @project unless project
+
+ current_rate = if @all_rates
+ at_date_in_project_with_ancestors(Date.today, all_rates, project)
+ else
+ current_rate = @user.current_rate(project)
+ end
+%>
+
+
+ <% if project && User.current.allowed_to?({:controller => '/hourly_rates', :action => 'edit'}, project) %>
+ <%= link_to l(:button_update), {:controller => '/hourly_rates', :action => 'edit', :project_id => project, :id => @user}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
+ <% end %>
+
+<%=h project.name %><%= (" - " + number_to_currency(current_rate.rate)) if current_rate %>
+<% if rates.empty? %>
+
+
+
+
+ <%= l(:label_nothing_display) %>
+
+
+
<%= l(:label_no_data) %>
+
+
+
+<% else %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%- rates.each do |rate| -%>
+
+ <%= rate.valid_from %>
+ <%= number_to_currency(rate.rate) %>
+ <%= rate == current_rate ? icon_wrapper('icon-context icon-checkmark',I18n.t(:general_text_Yes)) : "" %>
+
+ <%- end -%>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/_rate.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/_rate.html.erb
new file mode 100644
index 0000000000..088eab10a9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/_rate.html.erb
@@ -0,0 +1,50 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%-
+ index ||= "INDEX"
+ new_or_existing = rate.new_record? ? 'new' : 'existing'
+ id_or_index = rate.new_record? ? index : rate.id
+ prefix = "user[#{new_or_existing}_rate_attributes][]"
+ id_prefix = "user_#{new_or_existing}_rate_attributes_#{id_or_index}"
+ name_prefix = "user[#{new_or_existing}_rate_attributes][#{id_or_index}]"
+ classes ||= ""
+-%>
+
+<%= fields_for prefix, rate do |rate_form| %>
+
+
+ "><%= Rate.human_attribute_name(:valid_from) %>
+ <%= rate_form.text_field :valid_from, :size => 10, :class => 'date', :index => id_or_index %><%= calendar_for("#{id_prefix}_valid_from") %>
+
+
+
+ "><%= Rate.model_name.human %>
+ <%= rate_form.text_field :rate, :size => 7, :index => id_or_index, :value => rate.rate ? rate.rate.round(2) : "" %>
+
+ <%= Setting.plugin_openproject_costs['costs_currency'] %>
+
+
+
+
+ <%= icon_wrapper('icon-context icon-delete', I18n.t(:button_delete)) %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/edit.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/edit.html.erb
new file mode 100644
index 0000000000..c821d58d0e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/edit.html.erb
@@ -0,0 +1,88 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title l(:label_administration), "#{l(:label_edit)} #{l((@project.nil? ? :caption_default_rate_history_for : :caption_rate_history_for), user: ' ')} #{@user.name}" %>
+
+<%= toolbar title: (@project.nil? ? l(:caption_default_rate_history_for, :user => @user.name) : l(:caption_rate_history_for_project, :user => @user.name, :project => @project.name) ) %>
+
+<%- default_rate = @user.current_default_rate -%>
+<% if default_rate%>
+<%= l(:label_current_default_rate) %>: <%= number_to_currency(default_rate.rate)%>
+<% end %>
+
+<%= javascript_tag do -%>
+ RatesForm = new Subform('<%= escape_javascript(render(:partial => "rate", :object => HourlyRate.new )) %>',<%= @rates.length %>,'rates_body');
+<% end -%>
+
+<%= labelled_tabular_form_for @user, :url => {:action => 'update', :project_id => @project}, :method => :put do |f| %>
+ <%= back_url_hidden_field_tag %>
+ <%= error_messages_for 'user' %>
+ <%- @rates.each do |rate| -%>
+ <%- @rate = rate -%>
+ <%= error_messages_for 'rate' %>
+ <%- end -%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%- @rates.each_with_index do |rate, index| -%>
+ <%= render :partial => 'rate', :object => rate, :locals => {:index => index} %>
+ <%- end -%>
+
+
+
+
+
+
+ "><%= l(:description_date_for_new_rate) %>
+ <%= link_to_function l(:button_add_rate), "addRate($('add_rate_date'))", {class: "button -alt-highlight -with-icon icon-add"} %>
+ <%= styled_button_tag l(:button_save), class: '-with-icon icon-checkmark' %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/hourly_rates/show.html.erb b/vendored-plugins/openproject-costs/app/views/hourly_rates/show.html.erb
new file mode 100644
index 0000000000..b60ce5743a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/hourly_rates/show.html.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/costs_header' %>
+
+<% html_title l(:label_administration), l(:caption_rate_history_for, user: @user.name) %>
+
+<%= toolbar title: l(:caption_rate_history_for, :user => @user.name) %>
+
+<% if @project %>
+ <% html_title l(:caption_rate_history_for, user: @user.name) %>
+
+ <%= render :partial => 'list_project' %>
+<% else %>
+ <% html_title l(:label_administration), l(:caption_rate_history_for, user: @user.name) %>
+
+ <%= render :partial => 'list_default' %>
+ <%= render :partial => 'list' %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/settings/_openproject_costs.html.erb b/vendored-plugins/openproject-costs/app/views/settings/_openproject_costs.html.erb
new file mode 100644
index 0000000000..6d4561d4fb
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/settings/_openproject_costs.html.erb
@@ -0,0 +1,28 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+ <%= styled_label_tag :label_currency, l(:label_currency) %>
+ <%= styled_text_field_tag 'settings[costs_currency]', @settings['costs_currency'] %>
+
+
+
+ <%= styled_label_tag :label_currency_format, l(:label_currency_format) %>
+ <%= styled_text_field_tag 'settings[costs_currency_format]', @settings['costs_currency_format'] %>
+
diff --git a/vendored-plugins/openproject-costs/app/views/shared/_costs_header.html.erb b/vendored-plugins/openproject-costs/app/views/shared/_costs_header.html.erb
new file mode 100644
index 0000000000..383261e091
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/shared/_costs_header.html.erb
@@ -0,0 +1,24 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'costs/costs.css' %>
+ <%= javascript_include_tag 'costs/costs.js' %>
+<% end %>
diff --git a/vendored-plugins/openproject-costs/app/views/users/_rates.html.erb b/vendored-plugins/openproject-costs/app/views/users/_rates.html.erb
new file mode 100644
index 0000000000..f43e43837f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/app/views/users/_rates.html.erb
@@ -0,0 +1,28 @@
+<%#-- copyright
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= l(:caption_rate_history) %>
+<%
+ @rates = HourlyRate.history_for_user(@user)
+ @rates_default = @rates.delete(nil)
+%>
+
+<%= render :partial => 'hourly_rates/list_default' %>
+<%= render :partial => 'hourly_rates/list' %>
diff --git a/vendored-plugins/openproject-costs/config/locales/da.yml b/vendored-plugins/openproject-costs/config/locales/da.yml
new file mode 100644
index 0000000000..ecc3afbef8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/da.yml
@@ -0,0 +1,173 @@
+da:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Arbejdspakke
+ overridden_costs: Tilsidesatte omkostninger
+ spent: Forbrugt
+ spent_on: Dato
+ cost_object:
+ author: Forfatter
+ available: Tillrådighed
+ budget: Planlagte
+ budget_ratio: Forbrugt (ratio)
+ created_on: Oprettet d.
+ description: Beskrivelse
+ fixed_date: Fast dato
+ spent: Forbrugt
+ status: Status
+ subject: Emne
+ type: Omkostningstype
+ updated_on: Opdateret d.
+ cost_type:
+ unit: Enhedsnavn
+ unit_plural: Pluralistisk enhedsnavn
+ work_package:
+ cost_object_subject: Budgettitel
+ labor_costs: Lønomkostninger
+ material_costs: Enhedsomkostninger
+ overall_costs: Samlede omkostninger
+ spent_costs: Forbrugte omkostninger
+ spent_units: Forbrugte enheder
+ rate:
+ rate: Sats
+ user:
+ default_rates: Standardsatser
+ variable_cost_object:
+ labor_budget: Planlagte lønomkostninger
+ material_budget: Planlagte enhedsomkostninger
+ models:
+ cost_object: Budget
+ cost_type:
+ one: Cost type
+ other: Cost types
+ material_budget_item: Enhed
+ rate: Sats
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Arbejdspakke #%{id} er ikke et gyldigt mål til omfordeling af omkostningsangivelser.'
+ nullify_is_not_valid_for_cost_entries: Omkostningsangivelser kan ikke tildeles et projekt.
+ attributes:
+ budget: Planlagte omkostninger
+ comment: Kommentér
+ cost_object: Budget
+ cost_type: Omkostningstype
+ costs: Omkostninger
+ current_rate: Nuværende sats
+ hours: Timer
+ units: Enheder
+ valid_from: Gældende fra
+ button_add_budget_item: Tilføj planlagte omkostninger
+ button_add_cost_object: Tilføj budget
+ button_add_cost_type: Tilføj omkostningstype
+ button_add_rate: Tilføj sats
+ button_cancel_edit_budget: Annullér budgetredigering
+ button_cancel_edit_costs: Annullér omkostningsredigering
+ button_log_costs: Registrér enhedsomkostninger
+ button_log_time: Registrér tid
+ caption_booked_on_project: Reservationer til projekt
+ caption_default: Forhåndsvalgt
+ caption_default_rate_history_for: 'Standardsatshistorik for %{user}'
+ caption_labor: Arbejdskraft
+ caption_labor_costs: Faktiske lønomkostninger
+ caption_locked_on: Låst d.
+ caption_material_costs: Faktiske enhedsomkostninger
+ caption_materials: Enheder
+ caption_rate_history: Satshistorik
+ caption_rate_history_for: 'Satshistorik for %{user}'
+ caption_rate_history_for_project: 'Satshistorik for %{user} i projekt %{project}'
+ caption_save_rate: Gem sats
+ caption_set_rate: Indstil nuværende sats
+ caption_show_locked: Vis låste typer
+ cost_objects_title: Budgetter
+ label_cost_type_plural: Omkostningstyper
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Dato for ny sats
+ events:
+ cost_object: Budget redigeret
+ group_by_others: ikke i nogen gruppe
+ help_click_to_edit: Klik her for at redigere.
+ help_currency_format: 'Format for viste valutaværdier. %n erstattes med valutaværdien, %u erstattes med valutaenheden.'
+ help_override_rate: Angiv en værdi for at tilsidesætte standardsatsen.
+ label_between: mellem
+ label_cost_filter_add: Tilføj omkostningangivelsesfilter
+ label_costlog: Registrerede enhedsomkostninger
+ label_cost_object: Budget
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Nyt budget
+ label_cost_object_plural: Budgetter
+ label_cost_plural: Omkostninger
+ label_cost_report: Omkostningsrapport
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Omkostninger pr. side
+ label_currency: Valuta
+ label_currency_format: Valutaformat
+ label_current_default_rate: Nuværende standardsats
+ label_date_on: d.
+ label_deleted_cost_types: Slettede omkostningstyper
+ label_deliverable: Budget
+ label_display_cost_entries: Vis enhedsomkostninger
+ label_display_time_entries: Vis indberettede timer
+ label_display_types: Vis typer
+ label_edit: Redigér
+ label_fixed_cost_object: Fast budget
+ label_fixed_date: Fast dato
+ label_generic_user: Generisk bruger
+ label_greater_or_equal: '>='
+ label_group_by: Gruppér efter
+ label_group_by_add: Tilføj grupperingsfelt
+ label_hourly_rate: Timesats
+ label_include_deleted: Inkludér slettede
+ label_work_package_filter_add: Tilføj arbejdspakkefilter
+ label_kind: Type
+ label_less_or_equal: '<='
+ label_log_costs: Registrér enhedsomkostninger
+ label_no: Nej
+ label_option_plural: Valgmuligheder
+ label_overall_costs: Samlede omkostninger
+ label_rate: Sats
+ label_rate_plural: Satser
+ label_status_finished: Afsluttet
+ label_units: Omkostningsenheder
+ label_user: Bruger
+ label_until: indtil
+ label_valid_from: Gældende fra
+ label_variable_cost_object: Variabel sats baseret budget
+ label_view_all_cost_objects: Se alle budgetter
+ label_yes: Ja
+ notice_cost_object_conflict: Arbejdspakker skal være fra samme projekt.
+ notice_no_cost_objects_available: Ingen tilgængelige budgetter.
+ notice_something_wrong: Noget gik galt. Forsøg venligst igen.
+ notice_successful_restore: Gendannet.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Redigér reserverede enhedsomkostninger
+ permission_edit_cost_objects: Redigér budgetter
+ permission_edit_own_cost_entries: Redigér egne reserverede enhedsomkostninger
+ permission_edit_hourly_rates: Redigér timesatser
+ permission_edit_own_hourly_rate: Redigér egne timesatser
+ permission_edit_rates: Redigér satser
+ permission_log_costs: Reservationsenhedsomkostninger
+ permission_log_own_costs: Reservationsenhedsomkostninger for sig selv
+ permission_view_cost_entries: Se reservationsomkostninger
+ permission_view_cost_objects: Se budgetter
+ permission_view_cost_rates: Se omostningssatser
+ permission_view_hourly_rates: Se alle timesatser
+ permission_view_own_cost_entries: Se egen reservationsomkostninger
+ permission_view_own_hourly_rate: Se egen timestaser
+ permission_view_own_time_entries: Se eget tidsforbrug
+ project_module_costs_module: Omkostningsstyring
+ text_assign_time_and_cost_entries_to_project: Tildel indberettede timer og omkostninger til projektet
+ text_cost_object_change_type_confirmation: Sikker? Denne handling vil ødelægge data for den specifikke budgettype.
+ text_destroy_cost_entries_question: '%{cost_entries} blev indberettet for de arbejdspakker, som du er ved at slette. Hvad ønsker du at gøre?'
+ text_destroy_time_and_cost_entries: Slet indberettede timer og omkostninger
+ text_destroy_time_and_cost_entries_question: '%{hours} timer, %{cost_entries} blev indberettet på de arbejdspakker, som du er ved at slette. Hvad ønsker du at gøre?'
+ text_reassign_time_and_cost_entries: 'Gentildel indberettede timer og omkostninger til denne arbejdspakke:'
+ text_warning_hidden_elements: Visse angivelser kan været blevet ekskluderet fra sammenlægning.
+ week: uge
+ x_entries:
+ one: 1 entry
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: Sikker?
diff --git a/vendored-plugins/openproject-costs/config/locales/de.yml b/vendored-plugins/openproject-costs/config/locales/de.yml
new file mode 100644
index 0000000000..bc74dd448f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/de.yml
@@ -0,0 +1,173 @@
+de:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Arbeitspaket
+ overridden_costs: Überschriebene Kosten
+ spent: Gebucht
+ spent_on: Datum
+ cost_object:
+ author: Autor
+ available: Verfügbar
+ budget: Geplant
+ budget_ratio: Verbraucht (Prozent)
+ created_on: Erstellt
+ description: Beschreibung
+ fixed_date: Referenzdatum
+ spent: Gebucht
+ status: Status
+ subject: Thema
+ type: Kostenart
+ updated_on: Aktualisiert
+ cost_type:
+ unit: Einheit
+ unit_plural: Einheit plural
+ work_package:
+ cost_object_subject: Budgettitel
+ labor_costs: Personaleinzelkosten
+ material_costs: Stückkosten
+ overall_costs: Gesamtkosten
+ spent_costs: Gebuchte Kosten
+ spent_units: Gebuchte Einheiten
+ rate:
+ rate: Satz
+ user:
+ default_rates: Standardsätze
+ variable_cost_object:
+ labor_budget: Geplante Personaleinzelkosten
+ material_budget: Geplante Stückkosten
+ models:
+ cost_object: Budget
+ cost_type:
+ one: Kostenart
+ other: Kostenarten
+ material_budget_item: Stück
+ rate: Satz
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Arbeitspaket #%{id} ist kein gültiges Ziel für die Zuordnug der Stückkosteneinträge.'
+ nullify_is_not_valid_for_cost_entries: Stückkosten können keinem Projekt zugeordnet werden.
+ attributes:
+ budget: Geplante Kosten
+ comment: Kommentar
+ cost_object: Budget
+ cost_type: Kostenart
+ costs: Kosten
+ current_rate: Aktueller Satz
+ hours: Stunden
+ units: Einheiten
+ valid_from: Gültig ab
+ button_add_budget_item: Budgetposition hinzufügen
+ button_add_cost_object: Neues Budget anlegen
+ button_add_cost_type: Kostenart anlegen
+ button_add_rate: Satz hinzufügen
+ button_cancel_edit_budget: Budget bearbeiten abbrechen
+ button_cancel_edit_costs: Kosten bearbeiten abbrechen
+ button_log_costs: Stückkosten buchen
+ button_log_time: Zeit buchen
+ caption_booked_on_project: Auf das Projekt gebucht
+ caption_default: Standard
+ caption_default_rate_history_for: 'Standardsatz-Historie für %{user}'
+ caption_labor: Personal
+ caption_labor_costs: Ist-Personaleinzelkosten
+ caption_locked_on: Gesperrt am
+ caption_material_costs: Ist-Stückkosten
+ caption_materials: Einheiten
+ caption_rate_history: Stundensatz-Historie
+ caption_rate_history_for: 'Stundensatz-Historie für %{user}'
+ caption_rate_history_for_project: 'Stundensatz-Historie für %{user} in Projekt %{project}'
+ caption_save_rate: Satz speichern
+ caption_set_rate: Aktuellen Satz festlegen
+ caption_show_locked: Gesperrte Typen anzeigen
+ cost_objects_title: Budgets
+ label_cost_type_plural: Kostentypen
+ currency_delimiter: '.'
+ currency_separator: ','
+ description_date_for_new_rate: Datum für neuen Satz
+ events:
+ cost_object: Budget bearbeitet
+ group_by_others: In keiner der Gruppen
+ help_click_to_edit: Hier Klicken zum bearbeiten.
+ help_currency_format: 'Format der angezeigten Währungswerte. %n wird mit dem Zahlenwert ersetzt, %u mit der Währung.'
+ help_override_rate: Hier einen Wert eingeben um den Standardwert zu Überschreiben.
+ label_between: zwischen
+ label_cost_filter_add: Kostenfilter hinzufügen
+ label_costlog: Gebuchte Stückkosten
+ label_cost_object: Budget
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Neues Budget
+ label_cost_object_plural: Budgets
+ label_cost_plural: Kosten
+ label_cost_report: Report
+ label_cost_type_specific: 'Kostentyp #%{id}: %{name}'
+ label_costs_per_page: Kosten dieser Seite
+ label_currency: Währung
+ label_currency_format: Währungsformat
+ label_current_default_rate: Aktueller Standardsatz
+ label_date_on: am
+ label_deleted_cost_types: Gelöschte Kostenarten
+ label_deliverable: Budget
+ label_display_cost_entries: Stückkosten anzeigen
+ label_display_time_entries: Personaleinzelkosten anzeigen
+ label_display_types: Angezeigte Eintragsarten
+ label_edit: Bearbeiten
+ label_fixed_cost_object: Festes Budget
+ label_fixed_date: Referenzdatum
+ label_generic_user: Generischer Benutzer
+ label_greater_or_equal: '>='
+ label_group_by: Gruppiere Ergebnisse
+ label_group_by_add: Gruppierungsfeld hinzufügen
+ label_hourly_rate: Stundensatz
+ label_include_deleted: Gelöschte anzeigen
+ label_work_package_filter_add: Ticketfilter hinzufügen
+ label_kind: Art
+ label_less_or_equal: '<='
+ label_log_costs: Stückkosten buchen
+ label_no: Nein
+ label_option_plural: Optionen
+ label_overall_costs: Gesamtkosten
+ label_rate: Satz
+ label_rate_plural: Sätze
+ label_status_finished: Abgeschlossen
+ label_units: Einheiten
+ label_user: Benutzer
+ label_until: bis
+ label_valid_from: Gültig ab
+ label_variable_cost_object: Variables Budget
+ label_view_all_cost_objects: Alle Budgets anzeigen
+ label_yes: Ja
+ notice_cost_object_conflict: Tickets müssen im gleichen Projekt sein.
+ notice_no_cost_objects_available: Kein Budget verfügbar.
+ notice_something_wrong: Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.
+ notice_successful_restore: Erfolgreich wiederhergestellt.
+ notice_cost_logged_successfully: Stückkosten erfolgreich gebucht.
+ permission_edit_cost_entries: Bearbeiten gebuchter Stückkosten
+ permission_edit_cost_objects: Budgets bearbeiten
+ permission_edit_own_cost_entries: Bearbeiten eigener gebuchter Stückkosten
+ permission_edit_hourly_rates: Stundensätze bearbeiten
+ permission_edit_own_hourly_rate: Eigene Stundensätze bearbeiten
+ permission_edit_rates: Stundensätze editieren
+ permission_log_costs: Stückkosten buchen
+ permission_log_own_costs: Eigene Stückkosten buchen
+ permission_view_cost_entries: Gebuchte Kosten ansehen
+ permission_view_cost_objects: Budgets ansehen
+ permission_view_cost_rates: Anzeigen von Stückpreisen
+ permission_view_hourly_rates: Alle Stundensätze ansehen
+ permission_view_own_cost_entries: Eigene gebuchte Kosten ansehen
+ permission_view_own_hourly_rate: Eigene Stundensätze ansehen
+ permission_view_own_time_entries: Eigene gebuchte Aufwände ansehen
+ project_module_costs_module: Controlling
+ text_assign_time_and_cost_entries_to_project: Gebuchte Aufwände dem Projekt zuweisen
+ text_cost_object_change_type_confirmation: Sind Sie sicher? Diese Operation wird einige Informationen des aktuellen Budgets löschen.
+ text_destroy_cost_entries_question: 'Es wurden bereits %{cost_entries} auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?'
+ text_destroy_time_and_cost_entries: Gebuchte Aufwände löschen
+ text_destroy_time_and_cost_entries_question: 'Es wurden bereits %{hours} Stunden sowie %{cost_entries} auf dieses Ticket gebucht. Was soll mit den Aufwänden geschehen?'
+ text_reassign_time_and_cost_entries: 'Gebuchte Aufwände diesem Ticket zuweisen:'
+ text_warning_hidden_elements: Es wurden möglicherweise nicht alle Einträge berücksichtigt.
+ week: Woche
+ x_entries:
+ one: 1 Eintrag
+ other: '%{count} Einträge'
+ js:
+ text_are_you_sure: Sind Sie sicher?
diff --git a/vendored-plugins/openproject-costs/config/locales/en.yml b/vendored-plugins/openproject-costs/config/locales/en.yml
new file mode 100644
index 0000000000..6b39ca763a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/en.yml
@@ -0,0 +1,209 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+---
+en:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: "Work package"
+ overridden_costs: "Overridden costs"
+ spent: "Spent"
+ spent_on: "Date"
+ cost_object:
+ author: "Author"
+ available: "Available"
+ budget: "Planned"
+ budget_ratio: "Spent (ratio)"
+ created_on: "Created on"
+ description: "Description"
+ fixed_date: "Fixed date"
+ spent: "Spent"
+ status: "Status"
+ subject: "Subject"
+ type: "Cost type"
+ updated_on: "Updated on"
+ cost_type:
+ unit: "Unit name"
+ unit_plural: "Pluralized unit name"
+ work_package:
+ cost_object_subject: "Budget title"
+ labor_costs: "Labor costs"
+ material_costs: "Unit costs"
+ overall_costs: "Overall costs"
+ spent_costs: "Spent costs"
+ spent_units: "Spent units"
+ rate:
+ rate: "Rate"
+ user:
+ default_rates: "Default rates"
+ variable_cost_object:
+ labor_budget: "Planned labor costs"
+ material_budget: "Planned unit costs"
+ models:
+ cost_object: "Budget"
+ cost_type:
+ one: "Cost type"
+ other: "Cost types"
+ material_budget_item: "Unit"
+ rate: "Rate"
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: "Work package #%{id} is not a valid target for reassigning the cost entries."
+ nullify_is_not_valid_for_cost_entries: "Cost entries can not be assigned to a project."
+ attributes:
+ budget: "Planned costs"
+ comment: "Comment"
+ cost_object: "Budget"
+ cost_type: "Cost type"
+ costs: "Costs"
+ current_rate: "Current rate"
+ hours: "Hours"
+ units: "Units"
+ valid_from: "Valid from"
+
+ button_add_budget_item: "Add planned costs"
+ button_add_cost_object: "Add budget"
+ button_add_cost_type: "Add cost type"
+ button_add_rate: "Add rate"
+ button_cancel_edit_budget: "Cancel editing budget"
+ button_cancel_edit_costs: "Cancel editing costs"
+ button_log_costs: "Log unit costs"
+ button_log_time: "Log time"
+
+ caption_booked_on_project: "Booked on project"
+ caption_default: "Default"
+ caption_default_rate_history_for: "Default rate history for %{user}"
+ caption_labor: "Labor"
+ caption_labor_costs: "Actual labor costs"
+ caption_locked_on: "Locked on"
+ caption_material_costs: "Actual unit costs"
+ caption_materials: "Units"
+ caption_rate_history: "Rate history"
+ caption_rate_history_for: "Rate history for %{user}"
+ caption_rate_history_for_project: "Rate history for %{user} in project %{project}"
+ caption_save_rate: "Save rate"
+ caption_set_rate: "Set current rate"
+ caption_show_locked: "Show locked types"
+ cost_objects_title: "Budgets"
+ label_cost_type_plural: "Cost types"
+ currency_delimiter: ","
+ currency_separator: "."
+
+ description_date_for_new_rate: "Date for new rate"
+
+ events:
+ cost_object: "Budget edited"
+
+ group_by_others: "not in any group"
+
+ help_click_to_edit: "Click here to edit."
+ help_currency_format: "Format of displayed currency values. %n is replaced with the currency value, %u ist replaced with the currency unit."
+ help_override_rate: "Enter a value here to override the default rate."
+
+ label_between: "between"
+ label_cost_filter_add: "Add cost entry filter"
+ label_costlog: "Logged unit costs"
+ label_cost_object: "Budget"
+ label_cost_object_id: "Budget #%{id}"
+ label_cost_object_new: "New budget"
+ label_cost_object_plural: "Budgets"
+ label_cost_plural: "Costs"
+ label_cost_report: "Cost report"
+ label_cost_type_specific: "Budget #%{id}: %{name}"
+ label_cost_type_plural: "Cost types"
+ label_costs_per_page: "Costs per page"
+ label_currency: "Currency"
+ label_currency_format: "Format of currency"
+ label_current_default_rate: "Current default rate"
+ label_date_on: "on"
+ label_deleted_cost_types: "Deleted cost types"
+ label_locked_cost_types: "Locked cost types"
+ label_deliverable: "Budget"
+ label_display_cost_entries: "Display unit costs"
+ label_display_time_entries: "Display reported hours"
+ label_display_types: "Display types"
+ label_edit: "Edit"
+ label_fixed_cost_object: "Fixed budget"
+ label_fixed_date: "Fixed date"
+ label_generic_user: "Generic user"
+ label_greater_or_equal: ">="
+ label_group_by: "Group by"
+ label_group_by_add: "Add grouping field"
+ label_hourly_rate: "Hourly rate"
+ label_include_deleted: "Include deleted"
+ label_work_package_filter_add: "Add work package filter"
+ label_kind: "Type"
+ label_less_or_equal: "<="
+ label_log_costs: "Log unit costs"
+ label_no: "No"
+ label_option_plural: "Options"
+ label_overall_costs: "Overall costs"
+ label_rate: "Rate"
+ label_rate_plural: "Rates"
+ label_status_finished: "Finished"
+ label_units: "Cost units"
+ label_user: "User"
+ label_until: "until"
+ label_valid_from: "Valid from"
+ label_variable_cost_object: "Variable rate based budget"
+ label_view_all_cost_objects: "View all budgets"
+ label_yes: "Yes"
+
+ notice_cost_object_conflict: "WorkPackages must be of the same project."
+ notice_no_cost_objects_available: "No budgets available."
+ notice_something_wrong: "Something went wrong. Please try again."
+ notice_successful_restore: "Successful restore."
+ notice_successful_lock: "Locked successfully."
+ notice_cost_logged_successfully: 'Unit cost logged successfully.'
+
+ permission_edit_cost_entries: "Edit booked unit costs"
+ permission_edit_cost_objects: "Edit budgets"
+ permission_edit_own_cost_entries: "Edit own booked unit costs"
+ permission_edit_hourly_rates: "Edit hourly rates"
+ permission_edit_own_hourly_rate: "Edit own hourly rates"
+ permission_edit_rates: "Edit rates"
+ permission_log_costs: "Book unit costs"
+ permission_log_own_costs: "Book unit costs for oneself"
+ permission_view_cost_entries: "View booked costs"
+ permission_view_cost_objects: "View budgets"
+ permission_view_cost_rates: "View cost rates"
+ permission_view_hourly_rates: "View all hourly rates"
+ permission_view_own_cost_entries: "View own booked costs"
+ permission_view_own_hourly_rate: "View own hourly rate"
+ permission_view_own_time_entries: "View own spent time"
+ project_module_costs_module: "Cost control"
+
+ text_assign_time_and_cost_entries_to_project: "Assign reported hours and costs to the project"
+ text_cost_object_change_type_confirmation: "Are you sure? This operation will destroy information of the specific budget type."
+ text_destroy_cost_entries_question: "%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
+ text_destroy_time_and_cost_entries: "Delete reported hours and costs"
+ text_destroy_time_and_cost_entries_question: "%{hours} hours, %{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?"
+ text_reassign_time_and_cost_entries: "Reassign reported hours and costs to this work package:"
+ text_warning_hidden_elements: "Some entries may have been excluded from the aggregation."
+
+ week: "week"
+
+ x_entries:
+ one: "1 entry"
+ other: "%{count} entries"
+
+ js:
+ text_are_you_sure: "Are you sure?"
diff --git a/vendored-plugins/openproject-costs/config/locales/es-ES.yml b/vendored-plugins/openproject-costs/config/locales/es-ES.yml
new file mode 100644
index 0000000000..807fdd9192
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/es-ES.yml
@@ -0,0 +1,173 @@
+es:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Paquete de trabajo
+ overridden_costs: Reemplazado los costos
+ spent: Pasado
+ spent_on: Fecha
+ cost_object:
+ author: Autor
+ available: Disponible
+ budget: Planeado
+ budget_ratio: Gastado (cociente)
+ created_on: Creado en
+ description: Descripción
+ fixed_date: Fecha fija
+ spent: Pasado
+ status: Estado
+ subject: Asunto
+ type: Tipo de costo
+ updated_on: Actualizada el
+ cost_type:
+ unit: Nombre de la unidad
+ unit_plural: Nombre de la unidad pluralizado
+ work_package:
+ cost_object_subject: Título de presupuesto
+ labor_costs: Costos de mano de obra
+ material_costs: Costos unitarios
+ overall_costs: Costos totales
+ spent_costs: Costos gastados
+ spent_units: Unidades usadas
+ rate:
+ rate: Tasa
+ user:
+ default_rates: tasa de morosidad
+ variable_cost_object:
+ labor_budget: Costos laborales previstos
+ material_budget: Costos unitarios previstos
+ models:
+ cost_object: Presupuesto
+ cost_type:
+ one: Cost type
+ other: Cost types
+ material_budget_item: Unidad
+ rate: Tasa
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Paquete de trabajo #%{id} no es un objetivo válido para reasignar las entradas de costo.'
+ nullify_is_not_valid_for_cost_entries: 'Las entradas de costo no pueden ser asignadas a un proyecto.'
+ attributes:
+ budget: Costos previstos
+ comment: Comentario
+ cost_object: Presupuesto
+ cost_type: Tipo de costo
+ costs: Costos
+ current_rate: Tasa actual
+ hours: Horas
+ units: Unidades
+ valid_from: Válida desde
+ button_add_budget_item: Añadir los costos previstos
+ button_add_cost_object: Añadir presupuesto
+ button_add_cost_type: Agregar tipo de costo
+ button_add_rate: Añadir tasa
+ button_cancel_edit_budget: Cancelar edicion de presupuesto
+ button_cancel_edit_costs: Cancelar edición de los costos
+ button_log_costs: Registrar costos unitarios
+ button_log_time: Registro de tiempo
+ caption_booked_on_project: Reservados en proyecto
+ caption_default: Por defecto
+ caption_default_rate_history_for: 'Historial de morosidad para %{user}'
+ caption_labor: Mano de obra
+ caption_labor_costs: Costos laborales actuales
+ caption_locked_on: En la mira
+ caption_material_costs: Costos unitarios actuales
+ caption_materials: Unidades
+ caption_rate_history: Historial de tasa
+ caption_rate_history_for: 'Historial de tasa para %{user}'
+ caption_rate_history_for_project: 'Historial de tasa para %{user} en el proyecto %{project}'
+ caption_save_rate: Guardar tasa
+ caption_set_rate: Ajustar tasa actual
+ caption_show_locked: Mostrar tipos de bloqueados
+ cost_objects_title: Presupuestos
+ label_cost_type_plural: Tipos de costo
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Fecha para la nueva tarifa
+ events:
+ cost_object: Presupuesto editado
+ group_by_others: no en cualquier grupo
+ help_click_to_edit: Haga clic aquí para editar.
+ help_currency_format: 'Formato de muestra de valores de divisas. %n se sustituye por el valor de la moneda, %u es reemplazada por la unidad monetaria.'
+ help_override_rate: Introduzca un valor para anular la morisidad.
+ label_between: entre
+ label_cost_filter_add: Agregar filtro para entrada de costo
+ label_costlog: Costos unitarios registrados
+ label_cost_object: Presupuesto
+ label_cost_object_id: 'Presupuesto #%{id}'
+ label_cost_object_new: Nuevo presupuesto
+ label_cost_object_plural: Presupuestos
+ label_cost_plural: Costos
+ label_cost_report: Informe costo
+ label_cost_type_specific: 'Presupuesto %{id}:%{name}'
+ label_costs_per_page: Costos por página
+ label_currency: Moneda
+ label_currency_format: Formato de moneda
+ label_current_default_rate: tasa de morosidad actual
+ label_date_on: en
+ label_deleted_cost_types: Eliminar tipos de costo
+ label_deliverable: Presupuesto
+ label_display_cost_entries: Mostrar los costos unitarios
+ label_display_time_entries: Mostrar horas reportadas
+ label_display_types: Mostrar tipos
+ label_edit: Editar
+ label_fixed_cost_object: Presupuesto fijo
+ label_fixed_date: Fecha fija
+ label_generic_user: Usuario genérico
+ label_greater_or_equal: '>='
+ label_group_by: Agrupar por
+ label_group_by_add: Añadir campo de agrupamiento
+ label_hourly_rate: Tarifa por hora
+ label_include_deleted: Incluir eliminados
+ label_work_package_filter_add: Añadir filtro del paquete de trabajo
+ label_kind: Tipo
+ label_less_or_equal: '<='
+ label_log_costs: Registrar costos unitarios
+ label_no: 'No'
+ label_option_plural: Opciones
+ label_overall_costs: Costos totales
+ label_rate: Tasa
+ label_rate_plural: Tasas
+ label_status_finished: Acabado
+ label_units: Unidades de costo
+ label_user: Usuario
+ label_until: hasta
+ label_valid_from: Válida desde
+ label_variable_cost_object: Presupuesto de tasa variable
+ label_view_all_cost_objects: Ver todos los presupuestos
+ label_yes: Sí
+ notice_cost_object_conflict: Paquetes de trabajo deben de estar en un mismo proyecto.
+ notice_no_cost_objects_available: No hay presupuestos disponibles.
+ notice_something_wrong: Algo salió mal. Por favor intentelo nuevamente.
+ notice_successful_restore: Restauración exitosa.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Editar los costos unitarios reservados
+ permission_edit_cost_objects: Editar presupuestos
+ permission_edit_own_cost_entries: Editar los costos unitarios propios reservados
+ permission_edit_hourly_rates: Editar las tasas por hora
+ permission_edit_own_hourly_rate: Editar propias tarifas por hora
+ permission_edit_rates: Editar tasas
+ permission_log_costs: Reservar los costos unitarios
+ permission_log_own_costs: Reservar los costos unitarios para uno mismo
+ permission_view_cost_entries: Ver los costos reservados
+ permission_view_cost_objects: Ver los presupuestos
+ permission_view_cost_rates: Ver tasas de costos
+ permission_view_hourly_rates: Ver todas las tarifas por hora
+ permission_view_own_cost_entries: Ver los gastos reservados propios
+ permission_view_own_hourly_rate: Ver tarifa por hora propia
+ permission_view_own_time_entries: Ver tiempo propio utilizado
+ project_module_costs_module: Control de costos
+ text_assign_time_and_cost_entries_to_project: Asignar horas reportadas y los costos al proyecto
+ text_cost_object_change_type_confirmation: ¿Estás seguro? Esta operación va a destruir la información del presupuesto especificado.
+ text_destroy_cost_entries_question: '%{cost_entries} fueron reportados en los paquetes de trabajo que van a eliminar. ¿Qué quieres hacer?'
+ text_destroy_time_and_cost_entries: Eliminar horas reportadas y costos
+ text_destroy_time_and_cost_entries_question: '%{hours} horas, %{cost_entries} fueron reportados en el paquete de trabajo que va a eliminar.¿Qué quieres hacer?'
+ text_reassign_time_and_cost_entries: 'Reasignar horas y costos reportados para este paquete de trabajo:'
+ text_warning_hidden_elements: Algunas entradas han sido excluidas de la agregación.
+ week: semana
+ x_entries:
+ one: 1 entry
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: ¿Estás seguro?
diff --git a/vendored-plugins/openproject-costs/config/locales/fr.yml b/vendored-plugins/openproject-costs/config/locales/fr.yml
new file mode 100644
index 0000000000..862e7c62ca
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/fr.yml
@@ -0,0 +1,173 @@
+fr:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Lot de Travaux
+ overridden_costs: Coûts outrepassés
+ spent: Consommé
+ spent_on: Date
+ cost_object:
+ author: Auteur
+ available: Disponible
+ budget: Planifié
+ budget_ratio: Consommé (ratio)
+ created_on: Créé le
+ description: Description
+ fixed_date: Date fixe
+ spent: Consommé
+ status: Statut
+ subject: Sujet
+ type: Type de coût
+ updated_on: Mis à jour le
+ cost_type:
+ unit: "Nom de l'unité"
+ unit_plural: "Nom pluriel de l'unité"
+ work_package:
+ cost_object_subject: Titre du budget
+ labor_costs: "Coûts de main d'œuvre"
+ material_costs: Coûts unitaires
+ overall_costs: Coûts globaux
+ spent_costs: Montants dépensés
+ spent_units: Unités consommées
+ rate:
+ rate: Tarif
+ user:
+ default_rates: Tarifs par défaut
+ variable_cost_object:
+ labor_budget: "Coûts de main d'œuvre planifiés"
+ material_budget: Coûts unitaires estimés
+ models:
+ cost_object: Budget
+ cost_type:
+ one: Cost type
+ other: Cost types
+ material_budget_item: Unité
+ rate: Tarif
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: "Le lot de travaux #%{id} n'est pas une cible valide pour réaffecter les entrées de coût."
+ nullify_is_not_valid_for_cost_entries: Des entrées de coût ne peuvent pas être assignées à un projet.
+ attributes:
+ budget: Coûts estimés
+ comment: Commentaire
+ cost_object: Budget
+ cost_type: Type de coût
+ costs: Coûts
+ current_rate: Tarif actuel
+ hours: Heures
+ units: Unités
+ valid_from: Valable à partir du
+ button_add_budget_item: Ajouter des coûts planifiés
+ button_add_cost_object: Ajouter budget
+ button_add_cost_type: Ajouter type de coût
+ button_add_rate: Ajouter tarif
+ button_cancel_edit_budget: "Annuler l'édition du budget"
+ button_cancel_edit_costs: "Annuler l'édition des coûts"
+ button_log_costs: Consigner coûts unitaires
+ button_log_time: Consigner temps
+ caption_booked_on_project: Réservé pour le projet
+ caption_default: Défaut
+ caption_default_rate_history_for: 'Historique des tarifs par défaut pour %{user}'
+ caption_labor: "Main d'œuvre"
+ caption_labor_costs: "Coûts de main d'œuvre réels"
+ caption_locked_on: Verrouillé le
+ caption_material_costs: Coûts unitaires réels
+ caption_materials: Unités
+ caption_rate_history: Historique des tarifs
+ caption_rate_history_for: 'Historique des tarifs pour %{user}'
+ caption_rate_history_for_project: 'Historique des tarifs pour %{user} dans le projet %{project}'
+ caption_save_rate: Enregistrer tarif
+ caption_set_rate: Définir le tarif actuel
+ caption_show_locked: Afficher les types verrouillés
+ cost_objects_title: Budgets
+ label_cost_type_plural: Types de coût
+ currency_delimiter: ' '
+ currency_separator: ','
+ description_date_for_new_rate: Date pour le nouveau tarif
+ events:
+ cost_object: Budget modifié
+ group_by_others: dans aucun groupe
+ help_click_to_edit: Cliquez ici pour modifier.
+ help_currency_format: "Format des valeurs monétaires affichées. %n est remplacé par le montant, %u est remplacé par l'unité monétaire."
+ help_override_rate: Entrez une valeur ici pour outrepasser les tarifs de défaut.
+ label_between: entre
+ label_cost_filter_add: "Ajouter filtre d'entrée de coût"
+ label_costlog: Coûts unitaires consignés
+ label_cost_object: Budget
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Nouveau budget
+ label_cost_object_plural: Budgets
+ label_cost_plural: Coûts
+ label_cost_report: Rapport de coût
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Coûts par page
+ label_currency: Devise
+ label_currency_format: Format de devise
+ label_current_default_rate: Taux par défaut actuel
+ label_date_on: le
+ label_deleted_cost_types: Types de coût effacés
+ label_deliverable: Budget
+ label_display_cost_entries: Afficher les coûts unitaires
+ label_display_time_entries: Afficher les heures consignées
+ label_display_types: Afficher les types
+ label_edit: Éditer
+ label_fixed_cost_object: Budget fixe
+ label_fixed_date: Date fixe
+ label_generic_user: Utilisateur générique
+ label_greater_or_equal: '>='
+ label_group_by: Grouper par
+ label_group_by_add: Ajouter un champ de regroupement
+ label_hourly_rate: Tarif horaire
+ label_include_deleted: Inclure les supprimé(e)s
+ label_work_package_filter_add: Ajouter filtre de lot de travaux
+ label_kind: Type
+ label_less_or_equal: '<='
+ label_log_costs: Consigner coûts unitaires
+ label_no: Non
+ label_option_plural: Options
+ label_overall_costs: Coûts globaux
+ label_rate: Tarif
+ label_rate_plural: Tarifs
+ label_status_finished: Terminé
+ label_units: Unités de coût
+ label_user: Utilisateur
+ label_until: jusque
+ label_valid_from: Valable à partir du
+ label_variable_cost_object: Budget basé sur un tarif variable
+ label_view_all_cost_objects: Afficher tous les budgets
+ label_yes: Oui
+ notice_cost_object_conflict: Les lots de travaux doivent appartenir au même projet.
+ notice_no_cost_objects_available: Aucun budget disponible.
+ notice_something_wrong: "Quelque chose s'est mal passé. Veuillez réessayer."
+ notice_successful_restore: Restauration réussie.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Éditer coûts unitaires réservés
+ permission_edit_cost_objects: Éditer les budgets
+ permission_edit_own_cost_entries: Éditer coûts unitaires propres réservés
+ permission_edit_hourly_rates: Éditer les tarifs horaires
+ permission_edit_own_hourly_rate: Éditer ses propres tarifs horaires
+ permission_edit_rates: Éditer les tarifs
+ permission_log_costs: Réserver coûts unitaires
+ permission_log_own_costs: Réserver coûts unitaires propres
+ permission_view_cost_entries: Afficher coûts réservés
+ permission_view_cost_objects: Voir les budgets
+ permission_view_cost_rates: Voir les tarifs de coût
+ permission_view_hourly_rates: Voir tous les tarifs horaires
+ permission_view_own_cost_entries: Afficher coûts propres réservés
+ permission_view_own_hourly_rate: Voir son propre tarif horaire
+ permission_view_own_time_entries: Voir son propre temps passé
+ project_module_costs_module: Contrôle des coûts
+ text_assign_time_and_cost_entries_to_project: Assigner les heures et coûts consignés au projet
+ text_cost_object_change_type_confirmation: Êtes-vous sûr ? Cette opération détruira les informations spécifiques au type de budget.
+ text_destroy_cost_entries_question: '%{cost_entries} ont été consignés sur les lots de travaux que vous êtes sur le point de supprimer. Que voulez-vous faire ?'
+ text_destroy_time_and_cost_entries: Supprimer les heures et coûts consignés
+ text_destroy_time_and_cost_entries_question: '%{hours} heures, %{cost_entries} ont été consignés sur les lots de travaux que vous êtes sur le point de supprimer. Que voulez-vous faire ?'
+ text_reassign_time_and_cost_entries: 'Réassigner les heures et coûts consignés à ce lot de travaux :'
+ text_warning_hidden_elements: "Certaines entrées peuvent avoir été exclues de l'agrégation."
+ week: semaine
+ x_entries:
+ one: 1 entry
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: Êtes-vous sûr ?
diff --git a/vendored-plugins/openproject-costs/config/locales/id.yml b/vendored-plugins/openproject-costs/config/locales/id.yml
new file mode 100644
index 0000000000..18046f81c5
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/id.yml
@@ -0,0 +1,171 @@
+id:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Work Package
+ overridden_costs: Overridden costs
+ spent: Dihabiskan
+ spent_on: Tanggal
+ cost_object:
+ author: Pembuat
+ available: Tersedia
+ budget: Terencana
+ budget_ratio: Spent (rasio)
+ created_on: Dibuat pada
+ description: Deskripsi
+ fixed_date: Tanggal fix
+ spent: Dihabiskan
+ status: Status
+ subject: Subjek
+ type: Jenis biaya
+ updated_on: Diperbaharui pada
+ cost_type:
+ unit: Nama unit
+ unit_plural: Nama plural unit
+ work_package:
+ cost_object_subject: Judul anggaran
+ labor_costs: Biaya tenaga
+ material_costs: Biaya unit
+ overall_costs: Biaya keseluruhan
+ spent_costs: Biaya terpakai
+ spent_units: Unit terpakai
+ rate:
+ rate: Rate
+ user:
+ default_rates: Rate standart
+ variable_cost_object:
+ labor_budget: Rencana biaya tenaga
+ material_budget: Rencana biaya unit
+ models:
+ cost_object: Bugdet
+ cost_type:
+ other: Cost types
+ material_budget_item: Unit
+ rate: Rate
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Reassign biaya tidak dapat dilakukan pada Work package #%{id}.'
+ nullify_is_not_valid_for_cost_entries: Masukan biaya tidak dapat di-assign ke proyek.
+ attributes:
+ budget: Rencana biaya
+ comment: Komentar
+ cost_object: Bugdet
+ cost_type: Jenis biaya
+ costs: Biaya
+ current_rate: Rate saat ini
+ hours: Jam
+ units: Unit
+ valid_from: Berlaku dari
+ button_add_budget_item: Tambahkan biaya terencana
+ button_add_cost_object: Tambahkan budget
+ button_add_cost_type: Tambahkan jenis biaya
+ button_add_rate: Tambahkan rate
+ button_cancel_edit_budget: Batalkan edit budget
+ button_cancel_edit_costs: Batalkan edit biaya
+ button_log_costs: Log biaya unit
+ button_log_time: Log waktu
+ caption_booked_on_project: Telah dipesan pada proyek
+ caption_default: Default
+ caption_default_rate_history_for: 'Default rate history for %{user}'
+ caption_labor: Tenaga kerja
+ caption_labor_costs: Biaya tenaga aktual
+ caption_locked_on: Terkunci
+ caption_material_costs: Biaya unit aktual
+ caption_materials: Unit
+ caption_rate_history: Histori rate
+ caption_rate_history_for: 'Histori rate untuk %{user}'
+ caption_rate_history_for_project: 'Histori rate milik %{user} di proyek %{project}'
+ caption_save_rate: Simpan rate
+ caption_set_rate: Set rate saat ini
+ caption_show_locked: Tampilkan tipe yang terkunci
+ cost_objects_title: Budget
+ label_cost_type_plural: Jenis biaya
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Tanggal berlaku rate
+ events:
+ cost_object: Buget telah di-edit
+ group_by_others: tidak dalam grup manapun
+ help_click_to_edit: Klik untuk edit.
+ help_currency_format: 'Format of displayed currency values. %n is replaced with the currency value, %u ist replaced with the currency unit.'
+ help_override_rate: Masukkan nilai untuk mengubah rate default.
+ label_between: antara
+ label_cost_filter_add: Tambahkan filter entri biaya
+ label_costlog: Catatan biaya unit
+ label_cost_object: Bugdet
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Tambah budget
+ label_cost_object_plural: Budget
+ label_cost_plural: Biaya
+ label_cost_report: Laporan biaya
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Tampilan biaya perhalaman
+ label_currency: Mata Uang
+ label_currency_format: Format mata uang
+ label_current_default_rate: Rate saat ini
+ label_date_on: pada
+ label_deleted_cost_types: Jenis biaya terhapus
+ label_deliverable: Bugdet
+ label_display_cost_entries: Tampilkan biaya unit
+ label_display_time_entries: Tampilkan jumlah jam
+ label_display_types: Tampilkan tipe
+ label_edit: Edit
+ label_fixed_cost_object: Fixed budget
+ label_fixed_date: Tanggal fix
+ label_generic_user: User generik
+ label_greater_or_equal: '>='
+ label_group_by: Pengelompokan
+ label_group_by_add: Tambahkan grup
+ label_hourly_rate: Rate per-jam
+ label_include_deleted: Termasuk yang telah dihapus
+ label_work_package_filter_add: Tambahkan filter
+ label_kind: Tipe
+ label_less_or_equal: '<='
+ label_log_costs: Log biaya unit
+ label_no: 'No'
+ label_option_plural: Opsi
+ label_overall_costs: Biaya keseluruhan
+ label_rate: Rate
+ label_rate_plural: Rate
+ label_status_finished: Selesai
+ label_units: Biaya unit
+ label_user: User
+ label_until: hingga
+ label_valid_from: Berlaku dari
+ label_variable_cost_object: Variabel rate berdasarkan anggaran
+ label_view_all_cost_objects: Lihat semua budget
+ label_yes: 'Yes'
+ notice_cost_object_conflict: Pilihan WorksPackage harus dari proyek yang sama.
+ notice_no_cost_objects_available: Pilihan budget tidak tersedia.
+ notice_something_wrong: Terjadi suatu kesalahan. Silahkan coba lagi.
+ notice_successful_restore: Restore berhasil.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Ubah booked biaya unit
+ permission_edit_cost_objects: Edit budget
+ permission_edit_own_cost_entries: Edit booked biaya unit sendiri
+ permission_edit_hourly_rates: Edit rate per jam
+ permission_edit_own_hourly_rate: Edit rate per-jam milik sendiri
+ permission_edit_rates: Edit rate
+ permission_log_costs: Biaya unit pada buku
+ permission_log_own_costs: Biaya unit buku untuk dirinya sendiri
+ permission_view_cost_entries: Tampilkan booked biaya
+ permission_view_cost_objects: Tampilkan seluruh budget
+ permission_view_cost_rates: Tampilkan rate biaya
+ permission_view_hourly_rates: Tampilkan semua rate per-jam
+ permission_view_own_cost_entries: Tampilkan booked biaya sendiri
+ permission_view_own_hourly_rate: Tampilkan rate per-jam diri sendiri
+ permission_view_own_time_entries: Tampilan jumlah waktu diri sendiri
+ project_module_costs_module: Pengawasan biaya
+ text_assign_time_and_cost_entries_to_project: Masukkan laporan per-jam dan laporan biaya ke proyek
+ text_cost_object_change_type_confirmation: Operasi ini akan menghapus informasi pada tipe budget tertentu. Anda yakin?
+ text_destroy_cost_entries_question: '%{cost_entries} digunakan pada work package yang akan dihapus. Keputusan anda?'
+ text_destroy_time_and_cost_entries: Hapus jumlah jam dan biaya yang terlapor
+ text_destroy_time_and_cost_entries_question: '%{hours} jam, %{cost_entries} digunakan pada works package yang akan anda hapus. Keputusan anda?'
+ text_reassign_time_and_cost_entries: 'Assign ulang laporan jam dan biaya pada work package ini:'
+ text_warning_hidden_elements: Beberapa entri mungkin tidak dipakai oleh agregasi yang dilakukan.
+ week: minggu
+ x_entries:
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: Apakah anda yakin?
diff --git a/vendored-plugins/openproject-costs/config/locales/it.yml b/vendored-plugins/openproject-costs/config/locales/it.yml
new file mode 100644
index 0000000000..0e465d67b1
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/it.yml
@@ -0,0 +1,173 @@
+it:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Macro-attività (work package)
+ overridden_costs: Costi eccedenti
+ spent: Trascorso
+ spent_on: Data
+ cost_object:
+ author: Autore
+ available: Disponibile
+ budget: Pianificato
+ budget_ratio: Trascorso (tasso)
+ created_on: Creato il
+ description: Descrizione
+ fixed_date: Data fissa
+ spent: Trascorso
+ status: Stato
+ subject: Soggetto
+ type: Tipo di costo
+ updated_on: Aggiornato il
+ cost_type:
+ unit: "Nome dell'unità"
+ unit_plural: Nome unità pluralizzato
+ work_package:
+ cost_object_subject: Titolo del bilancio
+ labor_costs: Costi della manodopera
+ material_costs: Costi unitari
+ overall_costs: Costi complessivi
+ spent_costs: Costi consumati
+ spent_units: Unita consumate
+ rate:
+ rate: Tariffa
+ user:
+ default_rates: Tariffe di default
+ variable_cost_object:
+ labor_budget: Costo del lavoro pianificato
+ material_budget: Costi pianificati per unità
+ models:
+ cost_object: Bilancio
+ cost_type:
+ one: Tipo di costo
+ other: Tipi di costo
+ material_budget_item: Unità
+ rate: Tariffa
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'La macro-attività (work package) #%{id} non è una destinazione valida per riassegnare le entrate del costo.'
+ nullify_is_not_valid_for_cost_entries: Voci di costo non possono essere assegnate a un progetto.
+ attributes:
+ budget: Costi pianificati
+ comment: Commento
+ cost_object: Bilancio
+ cost_type: Tipo di costo
+ costs: Costi
+ current_rate: Tariffa attuale
+ hours: Ore
+ units: Unità
+ valid_from: Valido dal
+ button_add_budget_item: Aggiungi costi pianificati
+ button_add_cost_object: Aggiungi bilancio
+ button_add_cost_type: Aggiungi tipo di costo
+ button_add_rate: Aggiungi tariffa
+ button_cancel_edit_budget: Annulla modifica budget
+ button_cancel_edit_costs: Annulla modifica dei costi
+ button_log_costs: Registra unità di costo
+ button_log_time: Registra tempi
+ caption_booked_on_project: Prenotato su progetto
+ caption_default: Predefinito
+ caption_default_rate_history_for: 'Cronologia tariffe predefinite per %{user}'
+ caption_labor: Lavoro
+ caption_labor_costs: Costo del lavoro effettivo
+ caption_locked_on: Bloccato su
+ caption_material_costs: Costi effettivi unitari
+ caption_materials: Unità
+ caption_rate_history: Cronologia tariffe
+ caption_rate_history_for: 'Cronologia tariffe per %{user}'
+ caption_rate_history_for_project: 'Cronologia tariffe per %{user} nel progetto %{project}'
+ caption_save_rate: Salva tariffa
+ caption_set_rate: Impostata tariffa corrente
+ caption_show_locked: Mostra tipi bloccati
+ cost_objects_title: Bilanci
+ label_cost_type_plural: Tipi di costo
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Data per la nuova tariffa
+ events:
+ cost_object: Bilancio modificato
+ group_by_others: non in qualsiasi gruppo
+ help_click_to_edit: Clicca qui per modificare.
+ help_currency_format: "Formato di visualizzazione dei valori in valuta. %n viene sostituito con il valore della valuta, %u viene sostituito con l'unità di valuta."
+ help_override_rate: Immettere qui un valore per ignorare la tariffa di default.
+ label_between: tra
+ label_cost_filter_add: Aggiungi filtro sulla voce di costo
+ label_costlog: Costi unitari registrati
+ label_cost_object: Bilancio
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Nuovo budget
+ label_cost_object_plural: Bilanci
+ label_cost_plural: Costi
+ label_cost_report: Relazione sui costi
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Costi per pagina
+ label_currency: Valuta
+ label_currency_format: Formato della valuta
+ label_current_default_rate: Tariffa predefinita corrente
+ label_date_on: il
+ label_deleted_cost_types: Tipi di costo eliminati
+ label_deliverable: Bilancio
+ label_display_cost_entries: Mostra i costi unitari
+ label_display_time_entries: Mostra le ore segnalate
+ label_display_types: Mostra i tipi
+ label_edit: Modifica
+ label_fixed_cost_object: Budget fisso
+ label_fixed_date: Data fissa
+ label_generic_user: Utente generico
+ label_greater_or_equal: '>='
+ label_group_by: Raggruppa per
+ label_group_by_add: Aggiungi campo di raggruppamento
+ label_hourly_rate: Tariffa oraria
+ label_include_deleted: Includi eliminati
+ label_work_package_filter_add: Aggiungi filtro per macro-attività (work package)
+ label_kind: Tipo
+ label_less_or_equal: '<='
+ label_log_costs: Registra unità di costo
+ label_no: 'No'
+ label_option_plural: Opzioni
+ label_overall_costs: Costi complessivi
+ label_rate: Tariffa
+ label_rate_plural: Tariffe
+ label_status_finished: Terminato
+ label_units: Unità di costo
+ label_user: Utente
+ label_until: finché
+ label_valid_from: Valido dal
+ label_variable_cost_object: Budget in base a tariffe variabili
+ label_view_all_cost_objects: Mostra tutti i budget
+ label_yes: Si
+ notice_cost_object_conflict: La macro-attività deve essere dello stesso progetto.
+ notice_no_cost_objects_available: Nessun budget disponibile.
+ notice_something_wrong: Qualcosa è andato storto. Si prega di riprovare.
+ notice_successful_restore: Ripristino eseguito con successo.
+ notice_cost_logged_successfully: Costo unitario registrato con successo.
+ permission_edit_cost_entries: Modifica unità di costo riservate
+ permission_edit_cost_objects: Modifica budget
+ permission_edit_own_cost_entries: Modifica le proprie unità di costo riservate
+ permission_edit_hourly_rates: Modifica tariffe orarie
+ permission_edit_own_hourly_rate: Modifica le proprie tariffe orarie
+ permission_edit_rates: Modifica le tariffe
+ permission_log_costs: Riserva costi unitari
+ permission_log_own_costs: Riserva costi unitari per se stessi
+ permission_view_cost_entries: Visualizza costi riservati
+ permission_view_cost_objects: Visualizza budget
+ permission_view_cost_rates: Visualizza tariffe di costo
+ permission_view_hourly_rates: Mostra tutte le tariffe orarie
+ permission_view_own_cost_entries: Visualizza i propri costi prenotati
+ permission_view_own_hourly_rate: Visualizza propria tariffa oraria
+ permission_view_own_time_entries: Visualizzare il proprio tempo trascorso
+ project_module_costs_module: Controllo dei costi
+ text_assign_time_and_cost_entries_to_project: Assegna orari e costi segnalati al progetto
+ text_cost_object_change_type_confirmation: Sei sicuro? Questa operazione distruggerà le informazioni del tipo budget specificato.
+ text_destroy_cost_entries_question: '%{cost_entries} sono stati segnalati per la macro-attività (work package) che si sta per eliminare. Che cosa vuoi fare?'
+ text_destroy_time_and_cost_entries: Elimina orari e costi riportati
+ text_destroy_time_and_cost_entries_question: '%{hours} ore, %{cost_entries} sono stati segnalati per le macro-attività (work packages) che si stanno per eliminare. Che cosa vuoi fare?'
+ text_reassign_time_and_cost_entries: 'Riassegna orari e costi segnalati a questa macro-attività (work package):'
+ text_warning_hidden_elements: "Alcune voci potrebbero essere state escluse dall'aggregazione."
+ week: settimana
+ x_entries:
+ one: 1 voce
+ other: '%{count} voci'
+ js:
+ text_are_you_sure: Sei sicuro/a?
diff --git a/vendored-plugins/openproject-costs/config/locales/ja.yml b/vendored-plugins/openproject-costs/config/locales/ja.yml
new file mode 100644
index 0000000000..e9a82455ab
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/ja.yml
@@ -0,0 +1,171 @@
+ja:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: 作業項目
+ overridden_costs: オーバーライドされたコスト
+ spent: 使用済
+ spent_on: 日付
+ cost_object:
+ author: 作成者
+ available: 利用可能
+ budget: 計画
+ budget_ratio: 使用済 (比)
+ created_on: 作成日時
+ description: 説明
+ fixed_date: 固定日付
+ spent: 使用済
+ status: ステータス
+ subject: 題名
+ type: コスト種類
+ updated_on: 更新日時
+ cost_type:
+ unit: 単位名
+ unit_plural: 単位名
+ work_package:
+ cost_object_subject: 予算のタイトル
+ labor_costs: 作業員コスト
+ material_costs: 単価
+ overall_costs: 集計コスト
+ spent_costs: 使用済コスト
+ spent_units: 使用済単価
+ rate:
+ rate: 原価率
+ user:
+ default_rates: デフォルト原価率
+ variable_cost_object:
+ labor_budget: 計画作業員コスト
+ material_budget: 計画単価
+ models:
+ cost_object: 予算
+ cost_type:
+ other: Cost types
+ material_budget_item: ユニット
+ rate: 原価率
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'コストを再割り当てるには、作業項目#%{id}は対象外である。'
+ nullify_is_not_valid_for_cost_entries: コストエントリをプロジェクトに割り当てることはできません。
+ attributes:
+ budget: 計画コスト
+ comment: コメント
+ cost_object: 予算
+ cost_type: コスト種類
+ costs: コスト
+ current_rate: 現状原価率
+ hours: 時間
+ units: 単位
+ valid_from: から有効
+ button_add_budget_item: 計画コストを追加
+ button_add_cost_object: 予算を追加
+ button_add_cost_type: コストの種類を追加
+ button_add_rate: 原価率を追加
+ button_cancel_edit_budget: 予算の編集を取り消し
+ button_cancel_edit_costs: コストの編集を取り消し
+ button_log_costs: 単価を記録
+ button_log_time: 時間を記録
+ caption_booked_on_project: プロジェクトの予約
+ caption_default: デフォルト
+ caption_default_rate_history_for: '%{user} のデフォルト率の履歴'
+ caption_labor: 労働
+ caption_labor_costs: Actual labor costs
+ caption_locked_on: Locked on
+ caption_material_costs: Actual unit costs
+ caption_materials: 単位
+ caption_rate_history: Rate history
+ caption_rate_history_for: 'Rate history for %{user}'
+ caption_rate_history_for_project: 'Rate history for %{user} in project %{project}'
+ caption_save_rate: Save rate
+ caption_set_rate: Set current rate
+ caption_show_locked: Show locked types
+ cost_objects_title: Budgets
+ label_cost_type_plural: Cost types
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Date for new rate
+ events:
+ cost_object: Budget edited
+ group_by_others: not in any group
+ help_click_to_edit: Click here to edit.
+ help_currency_format: 'Format of displayed currency values. %n is replaced with the currency value, %u ist replaced with the currency unit.'
+ help_override_rate: Enter a value here to override the default rate.
+ label_between: between
+ label_cost_filter_add: Add cost entry filter
+ label_costlog: Logged unit costs
+ label_cost_object: 予算
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: New budget
+ label_cost_object_plural: Budgets
+ label_cost_plural: コスト
+ label_cost_report: Cost report
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Costs per page
+ label_currency: Currency
+ label_currency_format: Format of currency
+ label_current_default_rate: Current default rate
+ label_date_on: 'on'
+ label_deleted_cost_types: Deleted cost types
+ label_deliverable: 予算
+ label_display_cost_entries: Display unit costs
+ label_display_time_entries: Display reported hours
+ label_display_types: Display types
+ label_edit: Edit
+ label_fixed_cost_object: Fixed budget
+ label_fixed_date: 固定日付
+ label_generic_user: Generic user
+ label_greater_or_equal: '>='
+ label_group_by: Group by
+ label_group_by_add: Add grouping field
+ label_hourly_rate: Hourly rate
+ label_include_deleted: Include deleted
+ label_work_package_filter_add: Add work package filter
+ label_kind: Type
+ label_less_or_equal: '<='
+ label_log_costs: 単価を記録
+ label_no: 'No'
+ label_option_plural: Options
+ label_overall_costs: 集計コスト
+ label_rate: 原価率
+ label_rate_plural: Rates
+ label_status_finished: Finished
+ label_units: 単価
+ label_user: ユーザ
+ label_until: まで
+ label_valid_from: から有効
+ label_variable_cost_object: 可変率を基づいた予算
+ label_view_all_cost_objects: 全ての予算を表示
+ label_yes: はい
+ notice_cost_object_conflict: 作業項目は同一プロジェクトである必要があります。
+ notice_no_cost_objects_available: 予算がありません。
+ notice_something_wrong: エラーが発生しました。再試行してください。
+ notice_successful_restore: 復元処理が成功します。
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: 予約単価の編集
+ permission_edit_cost_objects: 予算の編集
+ permission_edit_own_cost_entries: 自分が予約した単価の編集
+ permission_edit_hourly_rates: 時給の編集
+ permission_edit_own_hourly_rate: 自分の時給の編集
+ permission_edit_rates: 率の編集
+ permission_log_costs: 単価の予約
+ permission_log_own_costs: 自分の単価の予約
+ permission_view_cost_entries: 予約したコストの表示
+ permission_view_cost_objects: 予算の表示
+ permission_view_cost_rates: コスト率の表示
+ permission_view_hourly_rates: 全ての時給の表示
+ permission_view_own_cost_entries: 自分が予約したコストの表示
+ permission_view_own_hourly_rate: 自分の時給の表示
+ permission_view_own_time_entries: 自分の使用済み時間の表示
+ project_module_costs_module: コスト・コントロール
+ text_assign_time_and_cost_entries_to_project: Assign reported hours and costs to the project
+ text_cost_object_change_type_confirmation: Are you sure? This operation will destroy information of the specific budget type.
+ text_destroy_cost_entries_question: '%{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?'
+ text_destroy_time_and_cost_entries: Delete reported hours and costs
+ text_destroy_time_and_cost_entries_question: '%{hours} hours, %{cost_entries} were reported on the work packages you are about to delete. What do you want to do ?'
+ text_reassign_time_and_cost_entries: 報告された時間とコストをこの作業項目に再割り当てください。
+ text_warning_hidden_elements: いくつかのエントリは、集計から除外されている可能性があります。
+ week: 週間
+ x_entries:
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: よろしいですか?
diff --git a/vendored-plugins/openproject-costs/config/locales/js-da.yml b/vendored-plugins/openproject-costs/config/locales/js-da.yml
new file mode 100644
index 0000000000..4aa3a32eae
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-da.yml
@@ -0,0 +1,12 @@
+da:
+ js:
+ work_packages:
+ property_groups:
+ costs: Omkostninger
+ properties:
+ costObject: Budget
+ overallCosts: Samlede omkostninger
+ spentUnits: Forbrugte enheder
+ button_log_costs: Registrér enhedsomkostninger
+ label_hour: time
+ label_hours: timer
diff --git a/vendored-plugins/openproject-costs/config/locales/js-de.yml b/vendored-plugins/openproject-costs/config/locales/js-de.yml
new file mode 100644
index 0000000000..28598b1ea4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-de.yml
@@ -0,0 +1,12 @@
+de:
+ js:
+ work_packages:
+ property_groups:
+ costs: Kosten
+ properties:
+ costObject: Budget
+ overallCosts: Gesamtkosten
+ spentUnits: Gebuchte Einheiten
+ button_log_costs: Stückkosten buchen
+ label_hour: Stunde
+ label_hours: Stunden
diff --git a/vendored-plugins/openproject-costs/config/locales/js-en.yml b/vendored-plugins/openproject-costs/config/locales/js-en.yml
new file mode 100644
index 0000000000..42c6bdee64
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-en.yml
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+en:
+ js:
+ work_packages:
+ property_groups:
+ costs: "Costs"
+ properties:
+ costObject: "Budget"
+ overallCosts: "Overall costs"
+ spentUnits: "Spent units"
+ button_log_costs: "Log unit costs"
+ label_hour: "hour"
+ label_hours: "hours"
diff --git a/vendored-plugins/openproject-costs/config/locales/js-es-ES.yml b/vendored-plugins/openproject-costs/config/locales/js-es-ES.yml
new file mode 100644
index 0000000000..a22cf09662
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-es-ES.yml
@@ -0,0 +1,12 @@
+es:
+ js:
+ work_packages:
+ property_groups:
+ costs: Costos
+ properties:
+ costObject: Presupuesto
+ overallCosts: Costos totales
+ spentUnits: Unidades usadas
+ button_log_costs: Registrar costos unitarios
+ label_hour: hora
+ label_hours: horas
diff --git a/vendored-plugins/openproject-costs/config/locales/js-fr.yml b/vendored-plugins/openproject-costs/config/locales/js-fr.yml
new file mode 100644
index 0000000000..3accb169dc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-fr.yml
@@ -0,0 +1,12 @@
+fr:
+ js:
+ work_packages:
+ property_groups:
+ costs: Coûts
+ properties:
+ costObject: Budget
+ overallCosts: Coûts globaux
+ spentUnits: Unités consommées
+ button_log_costs: Consigner coûts unitaires
+ label_hour: heure
+ label_hours: heures
diff --git a/vendored-plugins/openproject-costs/config/locales/js-id.yml b/vendored-plugins/openproject-costs/config/locales/js-id.yml
new file mode 100644
index 0000000000..cc86c7ed9c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-id.yml
@@ -0,0 +1,12 @@
+id:
+ js:
+ work_packages:
+ property_groups:
+ costs: Biaya
+ properties:
+ costObject: Bugdet
+ overallCosts: Biaya keseluruhan
+ spentUnits: Unit terpakai
+ button_log_costs: Log biaya unit
+ label_hour: jam
+ label_hours: jam
diff --git a/vendored-plugins/openproject-costs/config/locales/js-it.yml b/vendored-plugins/openproject-costs/config/locales/js-it.yml
new file mode 100644
index 0000000000..6168df5ed1
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-it.yml
@@ -0,0 +1,12 @@
+it:
+ js:
+ work_packages:
+ property_groups:
+ costs: Costi
+ properties:
+ costObject: Bilancio
+ overallCosts: Costi complessivi
+ spentUnits: Unita consumate
+ button_log_costs: Registra unità di costo
+ label_hour: ora
+ label_hours: ore
diff --git a/vendored-plugins/openproject-costs/config/locales/js-ja.yml b/vendored-plugins/openproject-costs/config/locales/js-ja.yml
new file mode 100644
index 0000000000..160b07c495
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-ja.yml
@@ -0,0 +1,12 @@
+ja:
+ js:
+ work_packages:
+ property_groups:
+ costs: コスト
+ properties:
+ costObject: 予算
+ overallCosts: 集計コスト
+ spentUnits: 使用済単価
+ button_log_costs: 単価を記録
+ label_hour: 時間
+ label_hours: 時間
diff --git a/vendored-plugins/openproject-costs/config/locales/js-pt-BR.yml b/vendored-plugins/openproject-costs/config/locales/js-pt-BR.yml
new file mode 100644
index 0000000000..4d0a4d3eaf
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-pt-BR.yml
@@ -0,0 +1,12 @@
+pt-BR:
+ js:
+ work_packages:
+ property_groups:
+ costs: Custos
+ properties:
+ costObject: Orçamento
+ overallCosts: Custos totais
+ spentUnits: Unidades gastas
+ button_log_costs: Registrar custos unitários
+ label_hour: hora
+ label_hours: horas
diff --git a/vendored-plugins/openproject-costs/config/locales/js-ru.yml b/vendored-plugins/openproject-costs/config/locales/js-ru.yml
new file mode 100644
index 0000000000..06f1b08707
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-ru.yml
@@ -0,0 +1,12 @@
+ru:
+ js:
+ work_packages:
+ property_groups:
+ costs: Расходы
+ properties:
+ costObject: Бюджет
+ overallCosts: Общие расходы
+ spentUnits: Затраченное количество
+ button_log_costs: Журналировать расходы на единицу
+ label_hour: час
+ label_hours: часы
diff --git a/vendored-plugins/openproject-costs/config/locales/js-sv-SE.yml b/vendored-plugins/openproject-costs/config/locales/js-sv-SE.yml
new file mode 100644
index 0000000000..6f7a77c4ef
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/js-sv-SE.yml
@@ -0,0 +1,12 @@
+sv:
+ js:
+ work_packages:
+ property_groups:
+ costs: Kostnader
+ properties:
+ costObject: Budget
+ overallCosts: Totalkostnader
+ spentUnits: Bokade enheter
+ button_log_costs: Logga enhetskostnader
+ label_hour: timme
+ label_hours: timmar
diff --git a/vendored-plugins/openproject-costs/config/locales/pt-BR.yml b/vendored-plugins/openproject-costs/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..01c6b90587
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/pt-BR.yml
@@ -0,0 +1,173 @@
+pt-BR:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Pacote de trabalho
+ overridden_costs: Substitui custos
+ spent: Gasto
+ spent_on: Data
+ cost_object:
+ author: Autor
+ available: Disponível
+ budget: Planejado
+ budget_ratio: Gasto (taxa)
+ created_on: Criado em
+ description: Descrição
+ fixed_date: Data fixa
+ spent: Gasto
+ status: Situação
+ subject: Assunto
+ type: Tipo de custo
+ updated_on: Atualizado em
+ cost_type:
+ unit: Nome da unidade
+ unit_plural: Plural do nome da unidade
+ work_package:
+ cost_object_subject: Título do orçamento
+ labor_costs: Custos de trabalho
+ material_costs: Custos unitários
+ overall_costs: Custos totais
+ spent_costs: Custos de gastos
+ spent_units: Unidades gastas
+ rate:
+ rate: Taxa
+ user:
+ default_rates: Taxas padrão
+ variable_cost_object:
+ labor_budget: Custos planejados de trabalho
+ material_budget: Custos unitários planejados
+ models:
+ cost_object: Orçamento
+ cost_type:
+ one: Tipo de custo
+ other: Tipos de custo
+ material_budget_item: Unidade
+ rate: Taxa
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Pacote de trabalho #%{id} não é válido para reatribuir as entradas de custo.'
+ nullify_is_not_valid_for_cost_entries: Custos dos insumos não podem ser atribuídos a um projeto.
+ attributes:
+ budget: Custos planejados
+ comment: Comentário
+ cost_object: Orçamento
+ cost_type: Tipo de custo
+ costs: Custos
+ current_rate: Taxa atual
+ hours: Horas
+ units: Unidades
+ valid_from: Válido a partir de
+ button_add_budget_item: Adicionar custos planejados
+ button_add_cost_object: Adicionar orçamento
+ button_add_cost_type: Adicionar o tipo de custo
+ button_add_rate: Adicionar taxa
+ button_cancel_edit_budget: Cancelar a edição de orçamento
+ button_cancel_edit_costs: Cancelar edição de custos
+ button_log_costs: Registrar custos unitários
+ button_log_time: Registrar tempo
+ caption_booked_on_project: Reservado para o projeto
+ caption_default: Padrão
+ caption_default_rate_history_for: 'Taxa histórica padrão para %{user}'
+ caption_labor: Trabalho
+ caption_labor_costs: Custos atuais do trabalho
+ caption_locked_on: Bloqueado
+ caption_material_costs: Custos unitários atuais
+ caption_materials: Unidades
+ caption_rate_history: Taxa histórica
+ caption_rate_history_for: 'Taxa histórica do %{user}'
+ caption_rate_history_for_project: 'Histórico de taxa para %{user} no projeto %{project}'
+ caption_save_rate: Salvar taxa
+ caption_set_rate: Fixar taxa atual
+ caption_show_locked: Mostrar tipos bloqueados
+ cost_objects_title: Orçamentos
+ label_cost_type_plural: Tipos de custo
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Data para nova taxa
+ events:
+ cost_object: Orçamento editado
+ group_by_others: Não em qualquer grupo
+ help_click_to_edit: Clique aqui para editar.
+ help_currency_format: 'Formato exibidos de valores de moeda. %n é substituído com o valor da moeda, %u é substituído com a unidade da moeda.'
+ help_override_rate: Insira um valor aqui para substituir a taxa padrão.
+ label_between: entre
+ label_cost_filter_add: Adicionar filtro de entrada de custo
+ label_costlog: Custos unitários registrados
+ label_cost_object: Orçamento
+ label_cost_object_id: 'Orçamento #%{id}'
+ label_cost_object_new: Novo orçamento
+ label_cost_object_plural: Orçamentos
+ label_cost_plural: Custos
+ label_cost_report: Relatório de custos
+ label_cost_type_specific: 'Orçamento #%{id}: %{name}'
+ label_costs_per_page: Custos por página
+ label_currency: Moeda
+ label_currency_format: Formato de moeda
+ label_current_default_rate: Atual taxa padrão
+ label_date_on: em
+ label_deleted_cost_types: Tipos de custos excluídos
+ label_deliverable: Orçamento
+ label_display_cost_entries: Exibir os custos unitários
+ label_display_time_entries: Exibir horas informadas
+ label_display_types: Exibir tipos
+ label_edit: Editar
+ label_fixed_cost_object: Orçamento fixo
+ label_fixed_date: Data fixa
+ label_generic_user: Usuário genérico
+ label_greater_or_equal: '>='
+ label_group_by: Agrupar por
+ label_group_by_add: Adicionar campo de agrupamento
+ label_hourly_rate: Taxa horária
+ label_include_deleted: Incluir excluídos
+ label_work_package_filter_add: Adicionar filtro de pacote de trabalho
+ label_kind: Tipo
+ label_less_or_equal: '<='
+ label_log_costs: Registrar custos unitários
+ label_no: Não
+ label_option_plural: Opções
+ label_overall_costs: Custos totais
+ label_rate: Taxa
+ label_rate_plural: Taxas
+ label_status_finished: Finalizado
+ label_units: Custos unitários
+ label_user: Usuário
+ label_until: até
+ label_valid_from: Válido a partir de
+ label_variable_cost_object: Orçamento baseado em taxa variável
+ label_view_all_cost_objects: Ver os todos os orçamentos
+ label_yes: Sim
+ notice_cost_object_conflict: Pacotes de trabalho devem ser do mesmo projeto.
+ notice_no_cost_objects_available: Não há orçamentos disponíveis.
+ notice_something_wrong: Algo deu errado. Por favor, tente novamente.
+ notice_successful_restore: Restauração bem-sucedida.
+ notice_cost_logged_successfully: Custo unitário registrado com êxito.
+ permission_edit_cost_entries: Editar custos unitários reservados
+ permission_edit_cost_objects: Editar os orçamentos
+ permission_edit_own_cost_entries: Editar custos unitários próprios reservados
+ permission_edit_hourly_rates: Editar taxas horárias
+ permission_edit_own_hourly_rate: Editar as próprias taxas horárias
+ permission_edit_rates: Editar taxas
+ permission_log_costs: Reservar custos unitários
+ permission_log_own_costs: Reservar custos unitários próprios
+ permission_view_cost_entries: Ver custos reservados
+ permission_view_cost_objects: Ver orçamentos
+ permission_view_cost_rates: Ver taxas de custo
+ permission_view_hourly_rates: Ver todas as taxas de horárias
+ permission_view_own_cost_entries: Ver custos reservados próprios
+ permission_view_own_hourly_rate: Ver sua própria taxa horária
+ permission_view_own_time_entries: Ver o próprio tempo gasto
+ project_module_costs_module: Controle de custo
+ text_assign_time_and_cost_entries_to_project: Atribuir horas relatadas e custos ao projeto
+ text_cost_object_change_type_confirmation: Tem certeza? Esta operação irá destruir informações do tipo específico de orçamento.
+ text_destroy_cost_entries_question: '%{cost_entries} foram informados sobre os pacotes de trabalho que você está prestes a excluir. O que você quer fazer?'
+ text_destroy_time_and_cost_entries: Excluir horas e custos informados
+ text_destroy_time_and_cost_entries_question: '%{hours} horas, %{cost_entries} foram relatados sobre os pacotes de trabalho que você está prestes a excluir. O que você quer fazer?'
+ text_reassign_time_and_cost_entries: 'Reatribua horas reportadas e custos para este pacote de trabalho:'
+ text_warning_hidden_elements: Algumas entradas podem ter sido excluídas da agregação.
+ week: semana
+ x_entries:
+ one: 1 entrada
+ other: '%{count} entradas'
+ js:
+ text_are_you_sure: Você tem certeza?
diff --git a/vendored-plugins/openproject-costs/config/locales/ru.yml b/vendored-plugins/openproject-costs/config/locales/ru.yml
new file mode 100644
index 0000000000..93eec6e1fd
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/ru.yml
@@ -0,0 +1,175 @@
+ru:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Пакет работ
+ overridden_costs: Переопределенные расходы
+ spent: Потрачено
+ spent_on: Дата
+ cost_object:
+ author: Автор
+ available: Доступно
+ budget: Запланированно
+ budget_ratio: Отработано (коэффициент)
+ created_on: Создано
+ description: Описание
+ fixed_date: Фиксированная Дата
+ spent: Потрачено
+ status: Состояние
+ subject: Тема
+ type: Тип расходов
+ updated_on: Обновлено
+ cost_type:
+ unit: Наименование количественного показателя
+ unit_plural: Множественное наименование количественного показателя
+ work_package:
+ cost_object_subject: Название бюджета
+ labor_costs: Трудовые расходы
+ material_costs: Расходы на единицу
+ overall_costs: Общие расходы
+ spent_costs: Понесенные расходы
+ spent_units: Затраченное количество
+ rate:
+ rate: Тариф
+ user:
+ default_rates: Тариф по умолчанию
+ variable_cost_object:
+ labor_budget: Запланированные трудозатраты
+ material_budget: Запланированные затраты по количеству
+ models:
+ cost_object: Бюджет
+ cost_type:
+ one: Cost type
+ few: Cost types
+ other: Cost types
+ material_budget_item: Единица
+ rate: Тариф
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Пакет работ #%{id} не является допустимым объектом для назначения стимостей.'
+ nullify_is_not_valid_for_cost_entries: Записи о стоимостях не могут быть присвоены проекту.
+ attributes:
+ budget: Запланированные расходы
+ comment: Комментарий
+ cost_object: Бюджет
+ cost_type: Тип расходов
+ costs: Расходы
+ current_rate: Текущий тариф
+ hours: Часы
+ units: Количество
+ valid_from: Действует с
+ button_add_budget_item: Добавить запланированные расходы
+ button_add_cost_object: Добавить бюджет
+ button_add_cost_type: Добавить тип расходов
+ button_add_rate: Добавить тариф
+ button_cancel_edit_budget: Отменить правку бюджета
+ button_cancel_edit_costs: Отменить правку расходов
+ button_log_costs: Журналировать расходы на единицу
+ button_log_time: Журналировать время
+ caption_booked_on_project: Заказано на проект
+ caption_default: По умолчанию
+ caption_default_rate_history_for: 'История тарифов по умолчанию для %{user}'
+ caption_labor: Трудовые
+ caption_labor_costs: Фактические трудозатраты
+ caption_locked_on: Закрывается на
+ caption_material_costs: Фактические затраты на единицу
+ caption_materials: Количество
+ caption_rate_history: История тарифов
+ caption_rate_history_for: 'История тарифов для %{user}'
+ caption_rate_history_for_project: 'История тарифов для %{user} в проекте %{project}'
+ caption_save_rate: Сохранить тариф
+ caption_set_rate: Установить текущий тариф
+ caption_show_locked: Показывать Заблокированные типы
+ cost_objects_title: Бюджеты
+ label_cost_type_plural: Типы расходов
+ currency_delimiter: ','
+ currency_separator: '.'
+ description_date_for_new_rate: Дата для нового тарифа
+ events:
+ cost_object: Бюджет изменен
+ group_by_others: не в какой-либо группе
+ help_click_to_edit: Нажмите здесь, чтобы править.
+ help_currency_format: 'Формат отображения денежных значений. %n заменяется значением валюты, %u заменяется денежной единицой.'
+ help_override_rate: Введите значение здесь для переопределения тарифа по умолчанию.
+ label_between: между
+ label_cost_filter_add: Добавить фильтр расходов
+ label_costlog: Журналированные затраты на единицу
+ label_cost_object: Бюджет
+ label_cost_object_id: 'Бюджет #%{id}'
+ label_cost_object_new: Новый бюджет
+ label_cost_object_plural: Бюджеты
+ label_cost_plural: Расходы
+ label_cost_report: Отчет по расходам
+ label_cost_type_specific: 'Бюджет #%{id}: %{name}'
+ label_costs_per_page: Расходов на странице
+ label_currency: Валюта
+ label_currency_format: Формат валюты
+ label_current_default_rate: Текущий тариф по умолчанию
+ label_date_on: на
+ label_deleted_cost_types: Типы удаленных расходов
+ label_deliverable: Бюджет
+ label_display_cost_entries: Отображать расходы на единицу
+ label_display_time_entries: Отображать часы по отчету
+ label_display_types: Отображение типов
+ label_edit: Правка
+ label_fixed_cost_object: Фиксированный бюджет
+ label_fixed_date: Фиксированная Дата
+ label_generic_user: Универсальный пользователь
+ label_greater_or_equal: '>='
+ label_group_by: Группировать по
+ label_group_by_add: Добавьте поле группировки
+ label_hourly_rate: Почасовой тариф
+ label_include_deleted: Включая удаленные
+ label_work_package_filter_add: Добавить фильтр пакетов работы
+ label_kind: Тип
+ label_less_or_equal: '<='
+ label_log_costs: Журналировать расходы на единицу
+ label_no: Нет
+ label_option_plural: Параметры
+ label_overall_costs: Общие расходы
+ label_rate: Тариф
+ label_rate_plural: Тарифы
+ label_status_finished: Завершенные
+ label_units: Стоимость единицы
+ label_user: Пользователь
+ label_until: До
+ label_valid_from: Действует с
+ label_variable_cost_object: Бюджет на основе переменного тарифа
+ label_view_all_cost_objects: Отобразить все бюджеты
+ label_yes: Да
+ notice_cost_object_conflict: Пакет работ должен быть из того же проекта.
+ notice_no_cost_objects_available: Нет доступных бюджетов.
+ notice_something_wrong: Произошла ошибка. Пожалуйста, попробуйте снова.
+ notice_successful_restore: Успешное восстановление.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Править зарегестрированные количественные затраты
+ permission_edit_cost_objects: Править бюджеты
+ permission_edit_own_cost_entries: Править собственные зарегистрированные количественные затраты
+ permission_edit_hourly_rates: Править почасовые тарифы
+ permission_edit_own_hourly_rate: Править собственные почасовые тарифы
+ permission_edit_rates: Править тарифы
+ permission_log_costs: Регистрировать затраты по количеству
+ permission_log_own_costs: Регистрировать затраты по количеству для себя
+ permission_view_cost_entries: Просмотр зарегистрированных затрат
+ permission_view_cost_objects: Просмотр бюджетов
+ permission_view_cost_rates: Просмотр тарифов по стоимости
+ permission_view_hourly_rates: Просмотр всех почасовых тарифов
+ permission_view_own_cost_entries: Просмотр своих зарегистрированных расходов
+ permission_view_own_hourly_rate: Просмотр своей почасовой ставки
+ permission_view_own_time_entries: Просмотр своего отработанного времени
+ project_module_costs_module: Контроль стоимости
+ text_assign_time_and_cost_entries_to_project: Связать сообщенные часы и расходы с проектом
+ text_cost_object_change_type_confirmation: Уверены? Эта операция уничтожит информацию о типе бюджета.
+ text_destroy_cost_entries_question: '%{cost_entries} сообщили о рабочих пакетах, которые вы собираетесь удалить. Что вы собираетесь сделать?'
+ text_destroy_time_and_cost_entries: Удалить отчеты о часах и расходах
+ text_destroy_time_and_cost_entries_question: '%{hours} часов, %{cost_entries} сообщили о рабочих пакетах, котрый вы собираетесь удалить. Что вы хотите сделать?'
+ text_reassign_time_and_cost_entries: 'Переназначьте сообщенные часы и расходы на этот пакет работ:'
+ text_warning_hidden_elements: Некоторые записи были исключены из агрегирования.
+ week: неделя
+ x_entries:
+ one: 1 entry
+ few: '%{count} entries'
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: Уверены?
diff --git a/vendored-plugins/openproject-costs/config/locales/sv-SE.yml b/vendored-plugins/openproject-costs/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..6790ef4ff0
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/locales/sv-SE.yml
@@ -0,0 +1,173 @@
+sv:
+ activerecord:
+ attributes:
+ cost_entry:
+ work_package: Arbetspaket
+ overridden_costs: Åsidosatta kostnader
+ spent: Spenderat
+ spent_on: Datum
+ cost_object:
+ author: Upphovsman
+ available: Tillgängliga
+ budget: Planerade
+ budget_ratio: Spenderade (förhållande)
+ created_on: Skapat den
+ description: Beskrivning
+ fixed_date: Referensdatum
+ spent: Spenderat
+ status: Status
+ subject: Ämne
+ type: Kostnadstyp
+ updated_on: Uppdaterat den
+ cost_type:
+ unit: Enhetsnamn
+ unit_plural: Enhetsnamn plural
+ work_package:
+ cost_object_subject: Budgettitel
+ labor_costs: Personalkostnader
+ material_costs: Enhetskostnader
+ overall_costs: Totalkostnader
+ spent_costs: Bokade kostnader
+ spent_units: Bokade enheter
+ rate:
+ rate: Timpris
+ user:
+ default_rates: Standardtimpris
+ variable_cost_object:
+ labor_budget: Planerade arbetskostnader
+ material_budget: Planerade enhetskostnader
+ models:
+ cost_object: Budget
+ cost_type:
+ one: Cost type
+ other: Cost types
+ material_budget_item: Enhet
+ rate: Timpris
+ errors:
+ models:
+ work_package:
+ is_not_a_valid_target_for_cost_entries: 'Arbetspaket #%{id} är inte ett giltigt mål för omfördelning av kostnadsposterna.'
+ nullify_is_not_valid_for_cost_entries: Kostnadsposterna kan inte tilldelas till ett projekt.
+ attributes:
+ budget: Planerade kostnader
+ comment: Kommentar
+ cost_object: Budget
+ cost_type: Kostnadstyp
+ costs: Kostnader
+ current_rate: Nuvarande timpris
+ hours: Timmar
+ units: Enheter
+ valid_from: Gäller från
+ button_add_budget_item: Lägg till planerade kostnader
+ button_add_cost_object: Lägg till budget
+ button_add_cost_type: Lägg till kostnadstyp
+ button_add_rate: Lägg till timpris
+ button_cancel_edit_budget: Avbryt redigering av budget
+ button_cancel_edit_costs: Avbryt redigering av kostnader
+ button_log_costs: Logga enhetskostnader
+ button_log_time: Logga tid
+ caption_booked_on_project: Bokade på projekt
+ caption_default: Standard
+ caption_default_rate_history_for: 'Historiskt standardtimpris för %{user}'
+ caption_labor: Arbetskraft
+ caption_labor_costs: Faktiska arbetskraftskostnader
+ caption_locked_on: Låst den
+ caption_material_costs: Faktiska enhetskostnader
+ caption_materials: Enheter
+ caption_rate_history: Historiska timpriser
+ caption_rate_history_for: 'Historiska timpriser för %{user}'
+ caption_rate_history_for_project: 'Historiska timpriser för %{user} i projekt %{project}'
+ caption_save_rate: Spara timpris
+ caption_set_rate: Ange aktuellt timpris
+ caption_show_locked: Visa låst typer
+ cost_objects_title: Budgetar
+ label_cost_type_plural: Kostnadstyper
+ currency_delimiter: ' '
+ currency_separator: ','
+ description_date_for_new_rate: Datum för nytt timpris
+ events:
+ cost_object: Budget redigerad
+ group_by_others: inte i någon grupp
+ help_click_to_edit: Klicka här för att redigera.
+ help_currency_format: 'Format på visade valutavärden. %n ersätts med valutavärdet, %u ersätts med valutaenheten.'
+ help_override_rate: Ange ett värde här om du vill åsidosätta standardtimpriset.
+ label_between: mellan
+ label_cost_filter_add: Lägg till filter för kostnadspost
+ label_costlog: Loggade enhetskostnader
+ label_cost_object: Budget
+ label_cost_object_id: 'Budget #%{id}'
+ label_cost_object_new: Ny budget
+ label_cost_object_plural: Budgetar
+ label_cost_plural: Kostnader
+ label_cost_report: Kostnadsrapport
+ label_cost_type_specific: 'Budget #%{id}: %{name}'
+ label_costs_per_page: Kostnader per sida
+ label_currency: Valuta
+ label_currency_format: Formatet för valuta
+ label_current_default_rate: Nuvarande standardtimpris
+ label_date_on: den
+ label_deleted_cost_types: Borttagna kostnadstyper
+ label_deliverable: Budget
+ label_display_cost_entries: Visa enhetskostnader
+ label_display_time_entries: Visa rapporterade timmar
+ label_display_types: Visa typer
+ label_edit: Redigera
+ label_fixed_cost_object: Fast budget
+ label_fixed_date: Referensdatum
+ label_generic_user: Generisk användare
+ label_greater_or_equal: '>='
+ label_group_by: Gruppera efter
+ label_group_by_add: Lägg till grupperingsfält
+ label_hourly_rate: Timpris
+ label_include_deleted: Inkludera borttagna
+ label_work_package_filter_add: Lägg till arbetspaketsfilter
+ label_kind: Typ
+ label_less_or_equal: '<='
+ label_log_costs: Logga enhetskostnader
+ label_no: Nej
+ label_option_plural: Alternativ
+ label_overall_costs: Totalkostnader
+ label_rate: Timpris
+ label_rate_plural: Priser
+ label_status_finished: Avslutad
+ label_units: Kostnadsenheter
+ label_user: Användare
+ label_until: fram till
+ label_valid_from: Gäller från
+ label_variable_cost_object: Variabla timpriser baserade på budget
+ label_view_all_cost_objects: Visa alla budgetar
+ label_yes: Ja
+ notice_cost_object_conflict: Arbetspaketen måste tillhöra samma projekt.
+ notice_no_cost_objects_available: Inga budgetar tillgängliga.
+ notice_something_wrong: Ett fel inträffade. Försök igen.
+ notice_successful_restore: Lyckad återställning.
+ notice_cost_logged_successfully: Unit cost logged successfully.
+ permission_edit_cost_entries: Redigera bokade enhetskostnader
+ permission_edit_cost_objects: Redigera budgetar
+ permission_edit_own_cost_entries: Redigera egna bokade enhetskostnader
+ permission_edit_hourly_rates: Redigera timpriser
+ permission_edit_own_hourly_rate: Redigera egna timpriser
+ permission_edit_rates: Redigera priser
+ permission_log_costs: Boka enhetskostnader
+ permission_log_own_costs: Boka enhetskostnader för sig själv
+ permission_view_cost_entries: Visa bokade kostnader
+ permission_view_cost_objects: Visa budgetar
+ permission_view_cost_rates: Visa enhetspriser
+ permission_view_hourly_rates: Visa alla timpriser
+ permission_view_own_cost_entries: Visa egna bokade kostnader
+ permission_view_own_hourly_rate: Visa egna timpriser
+ permission_view_own_time_entries: Visa egen spenderad tid
+ project_module_costs_module: Kostnadskontroll
+ text_assign_time_and_cost_entries_to_project: Tilldela rapporterade timmar och kostnader till projektet
+ text_cost_object_change_type_confirmation: Är du säker? Denna operation kommer att förstöra information av den specifika budgettypen.
+ text_destroy_cost_entries_question: '%{cost_entries} rapporterades på de arbetspaket som du håller på och tar bort. Vad vill du göra?'
+ text_destroy_time_and_cost_entries: Ta bort rapporterade timmar och kostnader
+ text_destroy_time_and_cost_entries_question: '%{hours} timmar, %{cost_entries} rapporterades på de arbetspaket som du håller på att ta bort. Vad vill du göra?'
+ text_reassign_time_and_cost_entries: 'Tilldela rapporterade timmar och kostnader på detta arbetspaket:'
+ text_warning_hidden_elements: Vissa poster kan ha undantagits från summeringen.
+ week: vecka
+ x_entries:
+ one: 1 entry
+ other: '%{count} entries'
+ js:
+ text_are_you_sure: Är du säker?
diff --git a/vendored-plugins/openproject-costs/config/routes.rb b/vendored-plugins/openproject-costs/config/routes.rb
new file mode 100644
index 0000000000..eb642dcf7f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/config/routes.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+OpenProject::Application.routes.draw do
+ scope 'projects/:project_id', as: 'projects' do
+ resources :cost_entries, controller: 'costlog', only: [:new, :create]
+
+ resources :cost_objects, only: [:new, :create, :index] do
+ post :update_labor_budget_item, on: :collection
+ post :update_material_budget_item, on: :collection
+ end
+
+ resources :hourly_rates, only: [:show, :edit, :update] do
+ post :set_rate, on: :member
+ end
+ end
+
+ scope 'work_packages/:work_package_id', as: 'work_packages' do
+ resources :cost_entries, controller: 'costlog', only: [:new, :index]
+ end
+
+ resources :cost_entries, controller: 'costlog', only: [:edit, :update, :destroy]
+
+ resources :cost_objects, only: [:show, :update, :destroy, :edit] do
+ get :copy, on: :member
+ end
+
+ resources :cost_types, only: [:index, :new, :edit, :update, :create, :destroy] do
+ member do
+ # TODO: check if this can be replaced with update method
+ put :set_rate
+ patch :restore
+ end
+ end
+
+ # TODO: this is a duplicate from a route defined under project/:project_id, check whether we really want to do that
+ resources :hourly_rates, only: [:edit, :update]
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20121022124254_aggregated_costs_migrations.rb b/vendored-plugins/openproject-costs/db/migrate/20121022124254_aggregated_costs_migrations.rb
new file mode 100644
index 0000000000..48a4e4e2f2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20121022124254_aggregated_costs_migrations.rb
@@ -0,0 +1,161 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'migration_squasher').to_s
+require Rails.root.join('db', 'migrate', 'migration_utils', 'setting_renamer').to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedCostsMigrations < ActiveRecord::Migration
+ def initialize(*)
+ super
+ @issues_table_exists = ActiveRecord::Base.connection.tables.include? 'issues'
+ end
+
+ MIGRATION_FILES = <<-MIGRATIONS
+ 001_create_deliverables.rb
+ 002_add_cost_types_default.rb
+ 003_add_deliverable_to_issues.rb
+ 004_add_rate.rb
+ 005_add_deliverables_date_fields.rb
+ 006_delete_cost_from_cost_entries.rb
+ 007_refactor_deliverable_intermediates.rb
+ 008_remove_budget_from_deliverable.rb
+ 009_add_comment_field_to_budget.rb
+ 010_add_free_text_to_budget_and_entries.rb
+ 011_add_deleted_at_to_cost_types.rb
+ 012_refactor_terms.rb
+ 013_create_cost_queries.rb
+ 014_add_denormalized_costs_fields.rb
+ 015_add_cost_query_variables.rb
+ 016_denormalize_spent_on_of_cost_entries.rb
+ 017_rename_permissions.rb
+ 018_higher_precision_for_currency.rb
+ 20091123144305_add_permission_inheritance.rb
+ 20091130214149_primary_key_for_groups_users.rb
+ 20091204172554_remove_costs_from_issues.rb
+ 20120313152442_create_initial_variable_cost_object_journals.rb
+ 20121022124253_remove_group_user_enhancements.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = 'redmine_costs'
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table 'cost_entries' do |t|
+ t.integer 'user_id', null: false
+ t.integer 'project_id', null: false
+ t.integer 'issue_id', null: false
+ t.integer 'cost_type_id', null: false
+ t.float 'units', null: false
+ t.date 'spent_on', null: false
+ t.datetime 'created_on', null: false
+ t.datetime 'updated_on', null: false
+ t.string 'comments', null: false
+ t.boolean 'blocked', default: false, null: false
+ t.decimal 'overridden_costs', precision: 15, scale: 4
+ t.decimal 'costs', precision: 15, scale: 4
+ t.integer 'rate_id'
+ t.integer 'tyear', null: false
+ t.integer 'tmonth', null: false
+ t.integer 'tweek', null: false
+ end
+
+ create_table 'cost_objects' do |t|
+ t.integer 'project_id', null: false
+ t.integer 'author_id', null: false
+ t.string 'subject', null: false
+ t.text 'description', null: false
+ t.string 'type', null: false
+ t.boolean 'project_manager_signoff', default: false, null: false
+ t.boolean 'client_signoff', default: false, null: false
+ t.date 'fixed_date', null: false
+ t.datetime 'created_on'
+ t.datetime 'updated_on'
+ end
+
+ create_table 'cost_types' do |t|
+ t.string 'name', null: false
+ t.string 'unit', null: false
+ t.string 'unit_plural', null: false
+ t.boolean 'default', default: false, null: false
+ t.datetime 'deleted_at'
+ end
+
+ create_table 'labor_budget_items' do |t|
+ t.integer 'cost_object_id', null: false
+ t.float 'hours', null: false
+ t.integer 'user_id'
+ t.string 'comments', default: '', null: false
+ t.decimal 'budget', precision: 15, scale: 4
+ end
+
+ create_table 'material_budget_items' do |t|
+ t.integer 'cost_object_id', null: false
+ t.float 'units', null: false
+ t.integer 'cost_type_id'
+ t.string 'comments', default: '', null: false
+ t.decimal 'budget', precision: 15, scale: 4
+ end
+
+ create_table 'rates' do |t|
+ t.date 'valid_from', null: false
+ t.decimal 'rate', precision: 15, scale: 4, null: false
+ t.string 'type', null: false
+ t.integer 'project_id'
+ t.integer 'user_id'
+ t.integer 'cost_type_id'
+ end
+
+ if @issues_table_exists
+ change_table 'issues' do |t|
+ t.column :cost_object_id, :integer, null: true
+ end
+ end
+
+ change_table 'time_entries' do |t|
+ t.decimal 'overridden_costs', precision: 15, scale: 4
+ t.decimal 'costs', precision: 15, scale: 4
+ t.integer 'rate_id'
+ end
+ TimeEntry.reset_column_information
+ Migration::SettingRenamer.rename('plugin_redmine_costs', 'plugin_openproject_costs')
+ end
+ end
+
+ def down
+ drop_table 'cost_entries'
+ drop_table 'cost_objects'
+ drop_table 'cost_queries'
+ drop_table 'cost_types'
+ drop_table 'labor_budget_items'
+ drop_table 'material_budget_items'
+ drop_table 'rates'
+ if @issues_table_exists
+ remove_column :issues, :cost_object_id
+ end
+
+ change_table 'time_entries' do |t|
+ t.remove_column 'overridden_costs'
+ t.remove_column 'costs'
+ t.remove_column 'rate_id'
+ end
+ TimeEntry.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130529145329_remove_signoff_from_cost_objects.rb b/vendored-plugins/openproject-costs/db/migrate/20130529145329_remove_signoff_from_cost_objects.rb
new file mode 100644
index 0000000000..178e6691df
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130529145329_remove_signoff_from_cost_objects.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class RemoveSignoffFromCostObjects < ActiveRecord::Migration
+ def change
+ remove_column :cost_objects, :project_manager_signoff
+ remove_column :cost_objects, :client_signoff
+ CostObject.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130625094710_add_costs_column_to_work_package.rb b/vendored-plugins/openproject-costs/db/migrate/20130625094710_add_costs_column_to_work_package.rb
new file mode 100644
index 0000000000..aa1667821f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130625094710_add_costs_column_to_work_package.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class AddCostsColumnToWorkPackage < ActiveRecord::Migration
+ def change
+ add_column :work_packages, :cost_object_id, :integer
+ WorkPackage.reset_column_information
+
+ rename_column :cost_entries, :issue_id, :work_package_id
+ CostEntry.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130916094369_legacy_issues_costs_data_to_work_packages.rb b/vendored-plugins/openproject-costs/db/migrate/20130916094369_legacy_issues_costs_data_to_work_packages.rb
new file mode 100644
index 0000000000..17ae41de6e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130916094369_legacy_issues_costs_data_to_work_packages.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'utils').to_s
+
+class LegacyIssuesCostsDataToWorkPackages < ActiveRecord::Migration
+ def up
+ return unless migration_applicable?
+
+ execute <<-SQL
+ UPDATE work_packages
+ SET cost_object_id = (SELECT legacy_issues.cost_object_id
+ FROM legacy_issues
+ WHERE legacy_issues.id = work_packages.id
+ LIMIT 1)
+ SQL
+ end
+
+ def down
+ return unless migration_applicable?
+
+ execute <<-SQL
+ UPDATE legacy_issues
+ SET cost_object_id = (SELECT work_packages.cost_object_id
+ FROM work_packages
+ WHERE work_packages.id = legacy_issues.id
+ LIMIT 1)
+ SQL
+ end
+
+ private
+
+ def migration_applicable?
+ ActiveRecord::Base.connection.table_exists?('legacy_issues') &&
+ ActiveRecord::Base.connection.columns('legacy_issues').map(&:name).include?('cost_object_id')
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130918084158_add_cost_object_journals.rb b/vendored-plugins/openproject-costs/db/migrate/20130918084158_add_cost_object_journals.rb
new file mode 100644
index 0000000000..c7c71ad9ad
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130918084158_add_cost_object_journals.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+class AddCostObjectJournals < ActiveRecord::Migration
+ def change
+ create_table :cost_object_journals do |t|
+ t.integer :journal_id, null: false
+ t.integer :project_id, null: false
+ t.integer :author_id, null: false
+ t.string :subject, null: false
+ t.text :description
+ t.date :fixed_date, null: false
+ t.datetime :created_on
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130918084919_add_cost_object_id_to_work_package_journals.rb b/vendored-plugins/openproject-costs/db/migrate/20130918084919_add_cost_object_id_to_work_package_journals.rb
new file mode 100644
index 0000000000..1da3d9d66e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130918084919_add_cost_object_id_to_work_package_journals.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class AddCostObjectIdToWorkPackageJournals < ActiveRecord::Migration
+ def change
+ add_column :work_package_journals, :cost_object_id, :integer, null: true
+ Journal::WorkPackageJournal.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130918160542_add_journal_columns_to_time_entry_journals.rb b/vendored-plugins/openproject-costs/db/migrate/20130918160542_add_journal_columns_to_time_entry_journals.rb
new file mode 100644
index 0000000000..7b2348a86b
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130918160542_add_journal_columns_to_time_entry_journals.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class AddJournalColumnsToTimeEntryJournals < ActiveRecord::Migration
+ def change
+ add_column :time_entry_journals, :overridden_costs, :decimal, precision: 15, scale: 2, null: true
+ add_column :time_entry_journals, :costs, :decimal, precision: 15, scale: 2, null: true
+ add_column :time_entry_journals, :rate_id, :integer
+ Journal::TimeEntryJournal.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-costs/db/migrate/20130918180542_legacy_variable_cost_object_journal_data.rb b/vendored-plugins/openproject-costs/db/migrate/20130918180542_legacy_variable_cost_object_journal_data.rb
new file mode 100644
index 0000000000..4ef4fb95d3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/db/migrate/20130918180542_legacy_variable_cost_object_journal_data.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2011-2013 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+#
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'legacy_journal_migrator').to_s
+
+class LegacyVariableCostObjectJournalData < ActiveRecord::Migration
+ def up
+ migrator.run
+ end
+
+ def down
+ migrator.remove_journals_derived_from_legacy_journals 'cost_object_journals'
+ end
+
+ def migrator
+ @migrator ||= Migration::LegacyJournalMigrator.new 'VariableCostObjectJournal', 'cost_object_journals' do
+ self.journable_class = 'CostObject'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/doc/CHANGELOG.md b/vendored-plugins/openproject-costs/doc/CHANGELOG.md
new file mode 100644
index 0000000000..074e3e552d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/doc/CHANGELOG.md
@@ -0,0 +1,135 @@
+
+
+# Changelog
+
+## 3.0.8 (new versions scheme)
+
+* `#8230` Missing Translation when deleting Cost Type
+* `#8233` Changing the default rate with invalid values
+* `#10232` Work package filter introduced by plugins not displayed
+
+## 5.0.4
+
+* `#4024` Fix: Subpages have no unique page titles
+* `#5357` Adapt released plugins to base on plugins functionality
+* Fix: Edit accesskey for budget
+* Adapted setting registration to changes in plugins plugin
+
+## 5.0.3
+
+* `#3440` New workpackage form layout
+* `#4024` Subpages have no unique page titles
+* `#4123` Add missing translation of save rate button
+* `#4112` Layouttabellen: Zwei Layouttabellen erschweren das Verständnis
+* `#4797` Fix: [Subdirectory] Broken Links
+* Updated to use cost formatter in openproject-xls_export 1.0.0.pre5
+
+## 5.0.2
+
+* Adapted to renaming of core method
+* use icons from icon font
+
+## 5.0.1
+
+* `#2259` [Accessibility] linearisation of issue show form (2)
+* `#2465` [Costs] Wrong link in ticket overview for budget
+* `#3065` [Work package tracking] Internal error when selecting costs in columns and displaying sums
+* `#3077` Public Release Costs plugin
+* `#3787` [Accessibility] Required fields MUST be displayed as required - new cost type
+* `#3862` Deleting fixed date results in internal error
+
+## 5.0.1.pre11
+
+* `#2250` [Accessibility] activity icon labels
+* `#2256` [Accessibility] no alt-texts in cost-types index view
+* `#2258` [Accessibility] linearisation of issue show form
+* removed not needed require of core css files
+* firxed cuke
+
+## 5.0.1.pre10
+
+* Adaptations for new icon font
+* `#1020` fix XSS when displaying costs
+* `#2591` Fix: Costs prevents work package context menu
+* `#2759` Fix: [Performance] Activity View very slow
+* `#3329` Refactor Duplicated Code Journals
+* added icon for new project menu
+
+## 5.0.1.pre9
+
+* Added missing translations
+
+## 5.0.1.pre8
+
+* `#2402` Squash old migrations
+* `#2545` Migrated old plugin settings
+* `#2570` Work package deletion no longer handles cost entries
+
+## 5.0.1.pre7
+
+* `#2376` Fix incorrect asset location.
+
+## 5.0.1.pre6
+
+* `#2070` Adaptions after changing core asset locations
+
+## 5.0.1.pre5
+
+* `#2050` Migrate to new data model.
+
+## 5.0.1.pre4 - 2013-07-11
+
+* Allows for assigning budgets to work_packages
+* Allows for assigning cost entries to work_packages
+
+## 5.0.1.pre3 - 2013-06-21
+
+* Removes csv and atom export for costlog (where non working)
+* Add missing translations
+* Adapt to OpenProject core changes
+
+## 5.0.1.pre2 - 2013-06-21
+
+* Use final plugin name schema
+* Removed reporting related code
+* Permission fix
+
+## 5.0.1.pre1 - 2013-05-24
+
+* fixed failing migrations when TimeEntry(s) exists
+* removed a lot of reporting-plugin patches
+* Translations fixes
+* added dependency to OpenProject core >= 3.0.0beta1
+
+## 5.0.1.beta - 2013-05-24
+
+* Fixed API requests for issues#destory
+* Added budget icon for activity view
+* Fixed budget overview table layout
+* Fixed costobject view
+* Removed signoff
+* Translations fixes
+* Enable enter key for the hourly rate forms
+
+## 5.0.0.rc1 - 2013-05-24
+
+* RC1 of the Rails 3 version
+* This version is no longer compatible with the Rails 2 core
diff --git a/vendored-plugins/openproject-costs/doc/COPYRIGHT.md b/vendored-plugins/openproject-costs/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..430fec61e9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject Costs Plugin
+
+This Plugin adds features for planning and tracking costs of projects.
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-costs/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-costs/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..46e8d4808f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/doc/COPYRIGHT_short.md
@@ -0,0 +1,16 @@
+OpenProject Costs Plugin
+
+Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-costs/doc/GPL.txt b/vendored-plugins/openproject-costs/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-costs/doc/apiv3.apib b/vendored-plugins/openproject-costs/doc/apiv3.apib
new file mode 100644
index 0000000000..a44237c207
--- /dev/null
+++ b/vendored-plugins/openproject-costs/doc/apiv3.apib
@@ -0,0 +1,483 @@
+FORMAT: 1A
+
+# Additions to OpenProject API v3
+
+This documentation lists the changes and additions that the costs plugin will
+introduce to the core OpenProject API.
+
+Unless otherwise stated it will only add endpoints and properties, but not remove anything that is
+already present in the core API.
+
+# Group Budgets
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:---------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
+| self | This budget | Budget | not null | READ |
+
+## Properties
+| Property | Description | Type | Constraints | Supported operations | Condition |
+| :---------: | ------------------------------------------- | ----------- | ----------- | -------------------- | --------------------------- |
+| id | Budget id | Integer | x > 0 | READ | |
+| subject | Budget name | String | not empty | READ | |
+
+*Note: Further properties of budgets might be added at a future date, however they will require the view budget permission to be displayed.*
+
+## Budget [/api/v3/budgets/{id}]
+
++ Model
+ + Body
+
+ {
+ "_type" : "Budget",
+ "_links" : {
+ "self" : {
+ "href" : "/api/v3/budgets/1",
+ "title" : "Q3 2015"
+ }
+ },
+ "id" : 1,
+ "subject" : "Q3 2015"
+ }
+
+
+## view Budget [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Budget id
+
++ Response 200 (application/hal+json)
+
+ [Budget][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view work packages **or** view budgets (on the budgets project)
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see this budget."
+ }
+
+## Budgets by Project [/api/v3/projects/{id}/budgets]
+
++ Model
+ + Body
+
+ {
+ "_links" : {
+ "self" : {
+ "href" : "/api/v3/projects/1/budgets"
+ }
+ },
+ "_type" : "Collection",
+ "total" : 2,
+ "count" : 2,
+ "_embedded" : {
+ "elements" : [
+ {
+ "_type" : "Budget",
+ "_links" : {
+ "self" : {
+ "href" : "/api/v3/budgets/1",
+ "title" : "Q3 2015"
+ }
+ },
+ "id" : 1,
+ "subject" : "Q3 2015"
+ },
+ {
+ "_type" : "Budget",
+ "_links" : {
+ "self" : {
+ "href" : "/api/v3/budgets/2",
+ "title" : "Q4 2015"
+ }
+ },
+ "id" : 2,
+ "subject" : "Q4 2015"
+ }
+ ]
+ }
+ }
+
+
+## view Budgets of a Project [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Project id
+
++ Response 200 (application/hal+json)
+
+ [Budgets by Project][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions to see the budgets of the given
+ project.
+
+ **Required permission:** view work packages **or** view budgets
+
+ *Note that you will only receive this error, if you are at least allowed to see the corresponding project.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see the budgets of this project."
+ }
+
++ Response 404 (application/hal+json)
+
+ Returned if either:
+
+ * the project does not exist
+ * the client does not have sufficient permissions to see the project
+ * the costs module is not enabled on the given project
+
+ **Required permission:** view project
+
+ *Note: A client without sufficient permissions shall not be able to test for the existence of a project.
+ That's why a 404 is returned here, even if a 403 might be more appropriate.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
+ "message": "The specified project does not exist."
+ }
+
+# Group Cost Entries
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:-----------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
+| self | This cost entry | CostEntry | not null | READ |
+| project | The project in which this entry was logged | Project | not null | READ |
+| costType | The type of this entry | CostType | not null | READ |
+| user | The user logging this entry | User | not null | READ |
+| workPackage | The work package that got the entry logged | WorkPackage | not null | READ |
+
+## Properties
+| Property | Description | Type | Constraints | Supported operations | Condition |
+| :---------: | ------------------------------------------- | ----------- | ----------- | -------------------- | --------------------------- |
+| id | cost entry id | Integer | x > 0 | READ | |
+| spentUnits | The amount of units logged in this entry | Float | | READ | |
+| spentOn | The date when the units were spent | Date | | READ | |
+| createdAt | Time of creation | DateTime | | READ | |
+| updatedAt | Time of the most recent change to the entry | DateTime | | READ | |
+
+## Cost Entry [/api/v3/cost_entries/{id}]
+
++ Model
+ + Body
+
+ {
+ "_type": "CostEntry",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_entries/1"
+ },
+ "project": {
+ "href": "/api/v3/projects/1"
+ },
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ },
+ "user": {
+ "href": "/api/v3/users/1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/1"
+ }
+ },
+ "id": 1,
+ "spentUnits": 3.14,
+ "spentOn": "2015-03-31",
+ "createdAt": "2015-03-31T08:51:20Z",
+ "updatedAt": "2015-03-31T08:51:20Z"
+ }
+
+
+## view Cost Entry [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Cost Entry id
+
++ Response 200 (application/hal+json)
+
+ [Cost Entry][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on cost entry's project)
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see this cost entry."
+ }
+
+## Cost Entries by work package [/api/v3/work_packages/{id}/cost_entries]
+
++ Model
+ + Body
+
+ {
+ "_links":
+ {
+ "self":
+ {
+ "href": "/api/v3/work_packages/1/cost_entries"
+ }
+ },
+ "total": 1,
+ "count": 1,
+ "_type": "Collection",
+ "_embedded":
+ {
+ "elements": [
+ {
+ "_type": "CostEntry",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_entries/1"
+ },
+ "project": {
+ "href": "/api/v3/projects/1"
+ },
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ },
+ "user": {
+ "href": "/api/v3/users/1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/1"
+ }
+ },
+ "id": 1,
+ "spentUnits": 3.14,
+ "spentOn": "2015-03-31",
+ "createdAt": "2015-03-31T08:51:20Z",
+ "updatedAt": "2015-03-31T08:51:20Z"
+ }
+ ]
+ }
+ }
+
+## list Cost Entries of a work package [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... work package id
+
++ Response 200 (application/hal+json)
+
+ [Cost Entries by work package][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on work package's project)
+
+ *Note that you will only receive this error, if you are at least allowed to see the corresponding work package.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see the cost entries of this work package."
+ }
+
++ Response 404 (application/hal+json)
+
+ Returned if the work package does not exist or the client does not have sufficient permissions
+ to see it.
+
+ **Required permission:** view work package
+
+ *Note: A client without sufficient permissions shall not be able to test for the existence of a work package.
+ That's why a 404 is returned here, even if a 403 might be more appropriate.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
+ "message": "The specified work package does not exist."
+ }
+
+## Work package costs per type [/api/v3/work_packages/{id}/summarized_costs_by_type]
+
+Returns a list of `AggregatedCostEntry`, with one entry per spent cost type.
+The spent units of all cost entries visible to the current user are summed up for each entry.
+
+An `AggregatedCostEntry` is a stripped down variant of a normal `CostEntry` which only has the link to
+a `CostType` and the amount of `spentUnits`.
+
+*Note that this is a preliminary endpoint. It is subject to future changes or removal.*
+
++ Model
+ + Body
+
+ {
+ "_links":
+ {
+ "self":
+ {
+ "href": "/api/v3/work_packages/1/summarized_costs_by_type"
+ }
+ },
+ "total": 1,
+ "count": 1,
+ "_type": "Collection",
+ "_embedded":
+ {
+ "elements": [
+ {
+ "_type": "AggregatedCostEntry",
+ "_links": {
+ "costType": {
+ "href": "/api/v3/cost_types/1"
+ }
+ },
+ "spentUnits": 31.4
+ }
+ ]
+ }
+ }
+
+## Show aggregated costs of a work package [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... work package id
+
++ Response 200 (application/hal+json)
+
+ [Work package costs per type][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on work package's project)
+
+ *Note that you will only receive this error, if you are at least allowed to see the corresponding work package.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see the cost entries of this work package."
+ }
+
++ Response 404 (application/hal+json)
+
+ Returned if the work package does not exist or the client does not have sufficient permissions
+ to see it.
+
+ **Required permission:** view work package
+
+ *Note: A client without sufficient permissions shall not be able to test for the existence of a work package.
+ That's why a 404 is returned here, even if a 403 might be more appropriate.*
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:NotFound",
+ "message": "The specified work package does not exist."
+ }
+
+# Group Cost Types
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:---------:|-------------------------------------------- | ------------- | --------------------- | -------------------- |
+| self | This cost type | CostType | not null | READ |
+
+## Properties
+| Property | Description | Type | Constraints | Supported operations | Condition |
+| :---------: | ------------------------------------------- | ----------- | ----------- | -------------------- | --------------------------- |
+| id | cost type id | Integer | x > 0 | READ | |
+| name | cost type name | String | not empty | READ | |
+| unit | The unit in which the costs are measured | String | not empty | READ | |
+| unitPlural | The pluralized form of the unit | String | not empty | READ | |
+| isDefault | `true` for the default type of new entries | Boolean | not null | READ | |
+
+## Cost Type [/api/v3/cost_types/{id}]
+
++ Model
+ + Body
+
+ {
+ "_type": "CostType",
+ "_links": {
+ "self": {
+ "href": "/api/v3/cost_types/1",
+ "title": "Energy cost"
+ }
+ },
+ "id": 1,
+ "name": "Energy cost",
+ "unit": "kWh",
+ "unitPlural": "kWh",
+ "isDefault": true
+ }
+
+
+## view Cost Type [GET]
+
++ Parameters
+ + id (required, integer, `1`) ... Cost Type id
+
++ Response 200 (application/hal+json)
+
+ [Cost Type][]
+
++ Response 403 (application/hal+json)
+
+ Returned if the client does not have sufficient permissions.
+
+ **Required permission:** view cost entries **or** view own cost entries (on any project)
+
+ + Body
+
+ {
+ "_type": "Error",
+ "errorIdentifier": "urn:openproject-org:api:v3:errors:MissingPermission",
+ "message": "You are not allowed to see cost types."
+ }
+
+# Group Work Packages
+
+The following properties are only added to work packages in projects where the costs module is activated.
+If the costs module is not available on a given work package, there will be no additional properties.
+
+## Linked Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:-----------:|-------------------------------------------- | --------------------------------- | --------------------- | -------------------- |
+| costObject | The budget associated to this work package | Budget | | READ / WRITE |
+| costsByType | List of accumulated costs per cost type | Collection of AggregatedCostEntry | | READ / WRITE |
+
+## Properties:
+| Link | Description | Type | Constraints | Supported operations |
+|:------------:|------------------------------------------------------------------- | ------------- | --------------------- | -------------------- |
+| overallCosts | The total amount of user visible costs logged on this work package | String | | READ / WRITE |
+
+`spentTime` has its visibility condition changed! It is now only visible when the client is either allowed to view time entries, or if
+he is allowed to see his own time entries in projects where the costs module is enabled.
diff --git a/vendored-plugins/openproject-costs/features/activity.feature b/vendored-plugins/openproject-costs/features/activity.feature
new file mode 100644
index 0000000000..d7ffaaf569
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/activity.feature
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Cost Object activities
+
+ Background:
+ Given there is a standard cost control project named "project1"
+ And I am already admin
+
+ Scenario: cost object is a selectable activity type
+ When I go to the activity page of the project "project1"
+ Then I should see "Budgets" within "#sidebar"
+
+ Scenario: Generating a cost object creates an activity
+ Given there is a variable cost object with the following:
+ | project | project1 |
+ | subject | Cost Object Subject |
+ | created_on | Time.now - 1.day |
+ When I go to the activity page of the project "project1"
+ And I activate activity filter "Cost Objects"
+ When I click "Apply"
+ Then I should see "Cost Object Subject"
+
+ Scenario: Updating a cost object creates an activity
+ Given there is a variable cost object with the following:
+ | project | project1 |
+ | subject | cost_object1 |
+ | created_on | Time.now - 40.days |
+ And I update the variable cost object "cost_object1" with the following:
+ | subject | cost_object1_new_title |
+ When I go to the activity page of the project "project1"
+ And I activate activity filter "Cost Objects"
+ When I click "Apply"
+ Then I should see "cost_object1_new_title"
+
+
diff --git a/vendored-plugins/openproject-costs/features/cost_types/deletion.feature b/vendored-plugins/openproject-costs/features/cost_types/deletion.feature
new file mode 100644
index 0000000000..823d39bb49
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/cost_types/deletion.feature
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Cost type deletion
+
+ Background:
+ Given there is 1 cost type with the following:
+ | name | cost_type1 |
+ And I am already admin
+
+ @javascript
+ Scenario: Deleting a cost type
+ When I delete the cost type "cost_type1"
+
+ Then the cost type "cost_type1" should not be listed on the index page
+
+ @javascript
+ Scenario: Deleted cost types are listed as deleted
+ When I delete the cost type "cost_type1"
+
+ Then the cost type "cost_type1" should be listed as deleted on the index page
+
+ @javascript
+ Scenario: Click on the "delete" link for a cost type
+ When I go to the index page of cost types
+
+ Then I expect to click "OK" on a confirmation box saying "Are you sure?"
+ And I click the delete link for the cost type "cost_type1"
+ And the confirmation box should have been displayed
diff --git a/vendored-plugins/openproject-costs/features/credit_unit_costs.feature b/vendored-plugins/openproject-costs/features/credit_unit_costs.feature
new file mode 100644
index 0000000000..f1d82415fc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/credit_unit_costs.feature
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Credit unit costs
+
+ Background:
+ Given there is a standard cost control project named "project1"
+ And the project "project1" has 1 issue with the following:
+ | subject | work_package1 |
+ And the role "Manager" may have the following rights:
+ | view_work_packages |
+ | edit_work_packages |
+ | view_work_packages |
+ | edit_work_packages |
+ | log_costs |
+ And there is 1 cost type with the following:
+ | name | cost_type_1 |
+ | unit | single_unit |
+ | unit_plural | multi_unit |
+
+ @javascript @wip
+ Scenario: Crediting units costs to an work_package
+ When I am already logged in as "manager"
+ And I go to the page of the work package "work_package1"
+ And I select "Log unit costs" from the action menu
+ And I fill in "cost_entry_units" with "100"
+ And I select "cost_type_1" from "Cost type"
+ And I press "Save"
+ Then I should be on the page of the work package "work_package1"
diff --git a/vendored-plugins/openproject-costs/features/manage_budget.feature b/vendored-plugins/openproject-costs/features/manage_budget.feature
new file mode 100644
index 0000000000..0b452fbb1f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/manage_budget.feature
@@ -0,0 +1,156 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Managing Budgets
+
+ Background:
+ Given there is 1 User with:
+ | Login | testuser |
+ | firstname | Chuck |
+ | lastname | Testa |
+ | default rate | 37 |
+ And there is 1 Project with the following:
+ | name | project1 |
+ | identifier | project1 |
+ And there is a role "manager"
+ And the role "manager" may have the following rights:
+ | edit_cost_objects |
+ | view_cost_rates |
+ | view_hourly_rates |
+ And there is 1 cost type with the following:
+ | name | cost_type_1 |
+ | unit | single_unit |
+ | unit_plural | multi_unit |
+ | cost_rate | 40 |
+ And the user "testuser" is a "manager" in the project "project1"
+ And I am already logged in as "testuser"
+
+@javascript
+ Scenario: Budgets with cost items can be created adding new cost items
+ When I go to the overview page of the project called "project1"
+ And I setup a budget with the following:
+ | subject | budget1 |
+ And I create a material item in row 1 with the following:
+ | units | 10 |
+ | comment | materialtestcomment |
+ Then the planned material costs in row 1 should be "400.00 EUR"
+ When I create a labor item in row 1 with the following:
+ | hours | 8 |
+ | comment | labortestcomment |
+ | user | Chuck Testa |
+ Then the planned labor costs in row 1 should be "296.00 EUR"
+ When I add a new material item
+ And I create a material item in row 2 with the following:
+ | units | 6 |
+ | comment | materialtestcomment2 |
+ Then the planned material costs in row 2 should be "240.00 EUR"
+ When I add a new labor item
+ And I create a labor item in row 2 with the following:
+ | hours | 5 |
+ | comment | labortestcomment2 |
+ | user | Chuck Testa |
+ Then the planned labor costs in row 2 should be "185.00 EUR"
+ When I create the budget
+ Then I should see "Successful creation"
+ And I should be on the show page for the budget "budget1"
+ And I should see "budget1" within ".cost_object"
+ And the stored planned material costs in row 1 should be "400.00 EUR"
+ And the stored planned labor costs in row 1 should be "296.00 EUR"
+ And the stored planned material costs in row 2 should be "240.00 EUR"
+ And the stored planned labor costs in row 2 should be "185.00 EUR"
+ And the stored total planned material costs should be "640.00 EUR"
+ And the stored total planned labor costs should be "481.00 EUR"
+
+@javascript
+ Scenario: Budgets can be updated with new cost items
+ Given there is a budget with the following:
+ | subject | budget1 |
+ | author | testuser |
+ | project | project1 |
+ And I go to the show page of the budget "budget1"
+ When I click on "Update"
+ And I create a material item in row 1 with the following:
+ | units | 10 |
+ | comment | materialtestcomment |
+ Then the planned material costs in row 1 should be "400.00 EUR"
+ When I create a labor item in row 1 with the following:
+ | hours | 8 |
+ | comment | labortestcomment |
+ | user | Chuck Testa |
+ Then the planned labor costs in row 1 should be "296.00 EUR"
+ When I click on "Submit"
+ Then I should see "Successful update"
+ And I should be on the show page for the budget "budget1"
+ And the stored planned material costs in row 1 should be "400.00 EUR"
+ And the stored planned labor costs in row 1 should be "296.00 EUR"
+ And the stored total planned material costs should be "400.00 EUR"
+ And the stored total planned labor costs should be "296.00 EUR"
+
+@javascript
+ Scenario: Budgets can be updated updating existing cost items
+ Given there is a budget with the following:
+ | subject | budget1 |
+ | author | testuser |
+ | project | project1 |
+ And the budget "budget1" has the following material items:
+ | units | comment | cost_type |
+ | 10 | materialtestcomment | cost_type_1 |
+ | 6 | materialtestcomment2 | cost_type_1 |
+ And the budget "budget1" has the following labor items:
+ | hours | comment | user |
+ | 8 | labortestcomment | testuser |
+ | 5 | labortestcomment2 | testuser |
+ And I go to the show page of the budget "budget1"
+ And I click on "Update"
+ And I update the material item in row 1 with the following:
+ | units | 5 |
+ | comment | changed_materialtestcomment |
+ Then the planned material costs in row 1 should be "200.00 EUR"
+ When I update the labor item in row 1 with the following:
+ | hours | 10 |
+ | comment | changed_labortestcomment |
+ | user | Chuck Testa |
+ Then the planned labor costs in row 1 should be "370.00 EUR"
+ When I click on "Submit"
+ Then I should see "Successful update"
+ And I should be on the show page for the budget "budget1"
+ And the stored planned material costs in row 1 should be "200.00 EUR"
+ And the stored planned labor costs in row 1 should be "370.00 EUR"
+
+@javascript
+ Scenario: Budgets can be copied
+ Given there is a budget with the following:
+ | subject | budget1 |
+ | author | testuser |
+ | project | project1 |
+ And the budget "budget1" has the following material items:
+ | units | comment | cost_type |
+ | 10 | materialtestcomment | cost_type_1 |
+ | 6 | materialtestcomment2 | cost_type_1 |
+ And the budget "budget1" has the following labor items:
+ | hours | comment | user |
+ | 8 | labortestcomment | testuser |
+ | 5 | labortestcomment2 | testuser |
+ And I go to the show page of the budget "budget1"
+ When I click on "Copy"
+ Then I should see "New budget"
+ And the planned material costs in row 1 should be "400.00 EUR"
+ And the planned labor costs in row 1 should be "296.00 EUR"
+ And the planned material costs in row 2 should be "240.00 EUR"
+ And the planned labor costs in row 2 should be "185.00 EUR"
diff --git a/vendored-plugins/openproject-costs/features/step_definitions/cost_object_steps.rb b/vendored-plugins/openproject-costs/features/step_definitions/cost_object_steps.rb
new file mode 100644
index 0000000000..bb06699532
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/step_definitions/cost_object_steps.rb
@@ -0,0 +1,105 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+When(/^I create a budget with the following:$/) do |table|
+ rows = table.rows_hash
+
+ steps %{
+ And I open the "Budgets" menu
+ And I follow "Add budget" within ".toolbar-container"
+ And I fill in "Subject" with "#{rows['subject']}"
+ }
+
+ click_button(I18n.t(:button_create), exact: true)
+end
+
+When(/^I create the budget$/) do
+ click_button(I18n.t(:button_create), exact: true)
+end
+
+When(/^I setup a budget with the following:$/) do |table|
+ rows = table.rows_hash
+
+ steps %{
+ And I open the "Budgets" menu
+ And I follow "Add budget" within ".toolbar-container"
+ And I fill in "Subject" with "#{rows['subject']}"
+ }
+end
+
+When(/^I (?:create|update) (?:a|the) (labor|material) item in row (\d+) with the following:$/) do |type, row_nr, table|
+ rows = table.rows_hash
+ unit = (type == 'labor') ? 'hours' : 'units'
+
+ page.find("##{type}_budget_items_body tr:nth-child(#{row_nr}) .units input").set(rows[unit])
+ page.find("##{type}_budget_items_body tr:nth-child(#{row_nr}) .comment input").set(rows['comment'])
+
+ if type == 'labor'
+ page.find(:xpath, "//tbody[@id='#{type}_budget_items_body']/tr[#{row_nr}]//option[contains(., '#{rows['user']}')]").select_option
+ end
+
+ # Here's why we need the following ugly hack of waiting two seconds.
+ #
+ # This step (When I create a labor item...) enters hours and selects a user.
+ # When doing this, for each change of these form field, an AJAX request will be sent to
+ # attempt to calculate the total costs. The hours field only checks once per second
+ # for a change (possibly to not send a request with each typed word), the user field
+ # immediately sends the request.
+ # This can lead to a scenario where our automation updates both hours and users field
+ # before the first request is sent (as it may wait a second). Thus, the first request
+ # already returns correct total costs and a following step finds the expected cost,
+ # (e.g. "296.00 EUR") and cucumber continues to submit the form. Once the form is submitted,
+ # the form field might detect a change and send an AJAX request (unnecessarily, but the
+ # field obviously doesn't know that). The Rails application first processes the form,
+ # adds a flash message to the session and returns a redirect to the show action.
+ # When Rails processes the second request (the AJAX request, not the show action),
+ # it clears the flash message without our base layout being able to show it.
+ # The following request to the show action then can't show the flash message
+ # and the expectation below for "Successful update" fails.
+ #
+ # Waiting 2 seconds gives the form field update JavaScript enough time to detect
+ # a change and send the AJAX request before the form is submitted.
+ #
+ # This fixes the following sometimes failing scenarios:
+ # * Budgets with cost items can be created adding new cost items
+ # * Budgets can be updated updating existing cost items
+ # * Budgets can be updated with new cost items
+ sleep 2
+end
+
+When(/^I add a new (labor|material) item$/) do |type|
+ steps %{ When I click on "Add planned costs" within "fieldset##{type}_budget_items_fieldset" }
+end
+
+Then (/^the planned (labor|material) costs in row (\d+) should be (.+)$/) do |type, row_nr, amount|
+ steps %{ Then I should see #{amount} within "##{type}_budget_items_body tr:nth-child(#{row_nr}) td.currency" }
+end
+
+Then (/^the stored planned (labor|material) costs in row (\d+) should be (.+)$/) do |type, row_nr, amount|
+ steps %{ Then I should see #{amount} within ".grid-content:first-child .#{type}_budget_items tbody tr:nth-child(#{row_nr}) td.currency" }
+end
+
+Then (/^the stored total planned (labor|material) costs should be (.+)$/) do |type, amount|
+ steps %{ Then I should see #{amount} within ".grid-content:first-child .#{type}_budget_items tfoot tr:last-child td.currency" }
+end
+
+Then (/^I should be able to update the budget "(.+)"$/) do |budget|
+ steps %{ Then I should be on the show page for the budget "#{budget}"
+ And I should see "Update" within "div#update" }
+end
diff --git a/vendored-plugins/openproject-costs/features/step_definitions/cost_steps.rb b/vendored-plugins/openproject-costs/features/step_definitions/cost_steps.rb
new file mode 100755
index 0000000000..2df3b30632
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/step_definitions/cost_steps.rb
@@ -0,0 +1,209 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Given /^the project "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |project, count|
+ p = Project.find_by_name(project) || Project.find_by_identifier(project)
+ as_admin count do
+ ce = FactoryGirl.build(:cost_entry)
+ ce.project = p
+ ce.work_package = FactoryGirl.create(:work_package, project: p)
+ ce.save!
+ end
+end
+
+Given /^there (?:is|are) (\d+) (default )?hourly rate[s]? with the following:$/ do |_num, is_default, table|
+ if is_default
+ hr = FactoryGirl.create(:default_hourly_rate)
+ else
+ hr = FactoryGirl.create(:hourly_rate)
+ end
+ send_table_to_object(hr, table, user: Proc.new do |rate, value|
+ unless rate.project.nil? || User.find_by_login(value).projects.include?(rate.project)
+ Rate.where(id: rate.id).update_all(project_id: User.find_by_login(value).projects(order: 'id ASC').last.id)
+ end
+ Rate.where(id: rate.id).update_all(user_id: User.find_by_login(value).id)
+ end,
+ valid_from: Proc.new do |rate, value|
+ # This works for definitions like "2 years ago"
+ number, time_unit, tempus = value.split
+ time = number.to_i.send(time_unit.to_sym).send(tempus.to_sym)
+ rate.update_attribute :valid_from, time
+ end)
+end
+
+Given /^the [Uu]ser "([^\"]*)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y)$/ do |user, count|
+ u = User.find_by_login user
+ p = u.projects.last
+ i = FactoryGirl.create(:work_package, project: p)
+ as_admin count do
+ ce = FactoryGirl.create(:cost_entry)
+ ce.user = u
+ ce.project = p
+ ce.work_package = i
+ ce.save!
+ end
+end
+
+Given /^the project "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the following:$/ do |project, count, table|
+ p = Project.find_by_name(project) || Project.find_by_identifier(project)
+ i = FactoryGirl.create(:work_package, project: p)
+ as_admin count do
+ ce = FactorGirl.build(:cost_entry)
+ ce.project = p
+ ce.work_package = i
+ send_table_to_object(ce, table)
+ ce.save!
+ end
+end
+
+Given /^the work package "([^\"]+)" has (\d+) [Cc]ost(?: )?[Ee]ntr(?:ies|y) with the following:$/ do |work_package_subject, count, table|
+ i = WorkPackage.where(subject: work_package_subject).last
+ as_admin count do
+ ce = FactoryGirl.build(:cost_entry, spent_on: (table.rows_hash['date'] ? table.rows_hash['date'].to_date : Date.today),
+ units: table.rows_hash['units'],
+ project: i.project,
+ work_package: i,
+ user: User.find_by_login(table.rows_hash['user']),
+ comments: 'lorem')
+
+ ce.cost_type = CostType.find_by_name(table.rows_hash['cost type']) if table.rows_hash['cost type']
+
+ ce.save!
+ end
+end
+
+Given /^there is a standard cost control project named "([^\"]*)"$/ do |name|
+ steps %{
+ Given there is 1 project with the following:
+ | Name | #{name} |
+ | Identifier | #{name.gsub(' ', '_').downcase} |
+ And the project "#{name}" has the following types:
+ | name |
+ | type1 |
+ And the project "#{name}" has 1 subproject
+ And the project "#{name}" has 1 issue with:
+ | subject | #{name}work_package |
+ And there is a role "Manager"
+ And the role "Manager" may have the following rights:
+ | view_own_hourly_rate |
+ | view_work_packages |
+ | view_work_packages |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_rates |
+ And there is a role "Controller"
+ And the role "Controller" may have the following rights:
+ | View own cost entries |
+ And there is a role "Developer"
+ And the role "Developer" may have the following rights:
+ | View own cost entries |
+ And there is a role "Reporter"
+ And the role "Reporter" may have the following rights:
+ | Create work packages |
+ And there is a role "Supplier"
+ And the role "Supplier" may have the following rights:
+ | View own hourly rate |
+ | View own cost entries |
+ And there is 1 user with:
+ | Login | manager |
+ And the user "manager" is a "Manager" in the project "#{name}"
+ And there is 1 user with:
+ | Login | controller |
+ And the user "controller" is a "Controller" in the project "#{name}"
+ And there is 1 user with:
+ | Login | developer |
+ And the user "developer" is a "Developer" in the project "#{name}"
+ And there is 1 user with:
+ | Login | reporter |
+ And the user "reporter" is a "Reporter" in the project "#{name}"
+ }
+end
+
+Given /^users have times and the cost type "([^\"]*)" logged on the work package "([^\"]*)" with:$/ do |cost_type, work_package, table|
+ i = WorkPackage.where(subject: work_package.subject).last
+ raise "No such work_package: #{work_package}" unless i
+
+ table.rows_hash.map do |k, v|
+ user = k.split.first
+ if k.end_with? 'hours'
+ steps %{
+ And the issue "#{work_package}" has 1 time entry with the following:
+ | hours | #{v} |
+ | user | #{user} |
+ }
+ elsif k.end_with? 'units'
+ steps %{
+ And the issue "#{work_package}" has 1 cost entry with the following:
+ | units | #{v} |
+ | user | #{user} |
+ | cost type | #{cost_type} |
+ }
+ elsif k.end_with? 'rate'
+ steps %{
+ And the user "#{user}" has:
+ | default rate | #{v} |
+ }
+ else
+ "Don't know what to do with #{k} => #{v}. Use | (hours|rate|units) | | as."
+ next
+ end
+ end
+end
+
+Given /^there is a (?:variable cost object|budget) with the following:$/ do |table|
+ cost_object = FactoryGirl.build(:variable_cost_object)
+
+ table_hash = table.rows_hash
+
+ cost_object.created_on = table_hash.has_key?('created_on') ?
+ eval(table_hash['created_on']) :
+ Time.now
+ cost_object.fixed_date = cost_object.created_on.to_date
+ cost_object.project = (Project.find_by_identifier(table_hash['project']) || Project.find_by_name(table_hash ['project'])) if table_hash.has_key? 'project'
+ cost_object.author = User.find_by_login(table_hash['author']) || cost_object.project.members.first.principal
+ cost_object.subject = table_hash['subject'] if table_hash.has_key? 'subject'
+
+ cost_object.save!
+ cost_object.journals.first.update_attribute(:created_at, eval(table_hash['created_on'])) if table_hash.has_key?('created_on')
+end
+
+Given /^I update the variable cost object "([^"]*)" with the following:$/ do |subject, table|
+ cost_object = VariableCostObject.find_by_subject(subject)
+
+ cost_object.subject = table.rows_hash['subject']
+ cost_object.save!
+end
+
+Given /^the (?:variable cost object|budget) "(.+)" has the following labor items:$/ do |subject, table|
+ cost_object = VariableCostObject.find_by_subject(subject)
+
+ table.hashes.each do |hash|
+ user = User.find_by_login(hash['user']) || User.find_by_name(hash['user']) || cost_object.project.members.first.principal
+ FactoryGirl.create(:labor_budget_item, user: user, cost_object: cost_object, comments: hash['comment'], hours: hash['hours'])
+ end
+end
+
+Given /^the (?:variable cost object|budget) "(.+)" has the following material items:$/ do |subject, table|
+ cost_object = VariableCostObject.find_by_subject(subject)
+
+ table.hashes.each do |hash|
+ cost_type = CostType.find_by_name(hash['cost_type']) || Cost_type.first
+ FactoryGirl.create(:material_budget_item, cost_type: cost_type, cost_object: cost_object, comments: hash['comment'], units: hash['units'])
+ end
+end
diff --git a/vendored-plugins/openproject-costs/features/step_definitions/cost_type_steps.rb b/vendored-plugins/openproject-costs/features/step_definitions/cost_type_steps.rb
new file mode 100644
index 0000000000..ff17d29725
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/step_definitions/cost_type_steps.rb
@@ -0,0 +1,86 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Given /^there is 1 cost type with the following:$/ do |table|
+ ct = FactoryGirl.build(:cost_type)
+ send_table_to_object(ct, table, cost_rate: Proc.new do |o, v|
+ FactoryGirl.create(:cost_rate, rate: v,
+ cost_type: o)
+ end,
+ name: Proc.new do |o, v|
+ o.name = v
+ o.unit = v
+ o.unit_plural = "#{v}s"
+ o.save!
+ end)
+end
+
+When(/^I delete the cost type "(.*?)"$/) do |name|
+ step %{I go to the index page of cost types}
+
+ ct = CostType.find_by_name name
+
+ within ("#delete_cost_type_#{ct.id}") do
+ find('a.submit_cost_type').click
+ end
+
+ if page.driver.is_a? Capybara::Selenium::Driver
+ # confirm "really delete?"
+ page.driver.browser.switch_to.alert.accept
+ end
+end
+
+When(/^I click the delete link for the cost type "(.*?)"$/) do |name|
+ ct = CostType.find_by_name name
+
+ within ("#delete_cost_type_#{ct.id}") do
+ find('a.submit_cost_type').click
+ end
+end
+
+When /^I expect to click "([^"]*)" on a confirmation box saying "([^"]*)"$/ do |option, message|
+ retval = (option == 'OK') ? 'true' : 'false'
+ page.evaluate_script("window.confirm = function (msg) {
+ document.cookie = msg
+ return #{retval}
+ }")
+ @expected_message = message.gsub('\\n', "\n")
+end
+
+When /^the confirmation box should have been displayed$/ do
+ expect(page.evaluate_script('document.cookie')).to include(@expected_message)
+end
+
+Then(/^the cost type "(.*?)" should not be listed on the index page$/) do |name|
+ if has_css?('.cost_types')
+ within '.cost_types' do
+ should_not have_link(name)
+ end
+ end
+end
+
+Then(/^the cost type "(.*?)" should be listed as deleted on the index page$/) do |name|
+ check(I18n.t(:caption_show_locked))
+
+ click_link(I18n.t(:button_apply))
+
+ within '.locked_cost_types' do
+ should have_text(name)
+ end
+end
diff --git a/vendored-plugins/openproject-costs/features/step_definitions/disabled_scenarios.rb b/vendored-plugins/openproject-costs/features/step_definitions/disabled_scenarios.rb
new file mode 100644
index 0000000000..354fa071fe
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/step_definitions/disabled_scenarios.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+ScenarioDisabler.disable(feature: 'Tracking Time', scenario: 'Adding a time entry')
+ScenarioDisabler.disable(feature: 'Tracking Time', scenario: 'Editing a time entry')
+ScenarioDisabler.disable(feature: 'Tracking Time', scenario: 'Selecting time period')
diff --git a/vendored-plugins/openproject-costs/features/support/path.rb b/vendored-plugins/openproject-costs/features/support/path.rb
new file mode 100644
index 0000000000..4e31dc6563
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/support/path.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the show page (?:of|for) the budget "(.+)?"$/
+ budget = CostObject.find_by_subject($1)
+ "/cost_objects/#{budget.id}"
+ when /^the index page (?:of|for) cost types$/
+ '/cost_types'
+ else
+ super
+ end
+ end
+end
+
+World(CostNavigationHelpers)
diff --git a/vendored-plugins/openproject-costs/features/view_own_rates.feature b/vendored-plugins/openproject-costs/features/view_own_rates.feature
new file mode 100644
index 0000000000..6810331e04
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/view_own_rates.feature
@@ -0,0 +1,84 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Permission View Own hourly and cost rates
+
+ @javascript
+ Scenario: Users that by set permission are only allowed to see their own rates, can not see the rates of others.
+ Given there is a standard cost control project named "Standard Project"
+ And the role "Supplier" may have the following rights:
+ | view_own_hourly_rate |
+ | view_work_packages |
+ | view_work_packages |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_rates |
+ | log_costs |
+ And there is 1 User with:
+ | Login | testuser |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ | default rate | 10.00 |
+ And the user "testuser" is a "Supplier" in the project "Standard Project"
+ And the project "Standard Project" has 1 issue with the following:
+ | subject | test_work_package |
+ And the issue "test_work_package" has 1 time entry with the following:
+ | hours | 1.00 |
+ | user | testuser |
+ And there is 1 cost type with the following:
+ | name | Translation |
+ | cost rate | 7.00 |
+ And the work package "test_work_package" has 1 cost entry with the following:
+ | units | 2.00 |
+ | user | testuser |
+ | cost type | Translation |
+ And the user "manager" has:
+ | hourly rate | 11.00 |
+ And the issue "test_work_package" has 1 time entry with the following:
+ | hours | 3.00 |
+ | user | manager |
+ And the work package "test_work_package" has 1 cost entry with the following:
+ | units | 5.00 |
+ | user | manager |
+ | cost type | Translation |
+ And I am already logged in as "testuser"
+ When I am on the page for the issue "test_work_package"
+ Then I should see "1 hour"
+ And I should see "2 Translations"
+ And I should see "24.00 EUR"
+ And I should not see "33.00 EUR" # labour costs only of Manager
+ And I should not see "35.00 EUR" # material costs only of Manager
+ And I should not see "43.00 EUR" # labour costs of me and Manager
+ And I should not see "49.00 EUR" # material costs of me and Manager
+ When I am on the work_packages page for the project called "Standard Project"
+ # ensure the page is loaded before opening the columns dropdown. Otherwise
+ # there will be no columns to choose from.
+ And I should see "status" within ".work-package-table--container"
+ And I choose "Columns" from the toolbar "settings" dropdown
+ And I select to see columns
+ | Overall costs |
+ | Labor costs |
+ | Unit costs |
+ And I click "Apply"
+ Then I should see "EUR 24.00"
+ And I should see "EUR 10.00"
+ And I should see "EUR 14.00"
+ And I should not see "EUR 33.00" # labour costs only of Manager
+ And I should not see "EUR 35.00" # material costs only of Manager
+ And I should not see "EUR 43.00" # labour costs of me and Manager
diff --git a/vendored-plugins/openproject-costs/features/work_packages/destroy_with_cost_entries.feature b/vendored-plugins/openproject-costs/features/work_packages/destroy_with_cost_entries.feature
new file mode 100644
index 0000000000..b91bee0694
--- /dev/null
+++ b/vendored-plugins/openproject-costs/features/work_packages/destroy_with_cost_entries.feature
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Deleting work packages
+ Background:
+ Given there is 1 user with:
+ | login | manager |
+ And there are the following types:
+ | Name | Is milestone |
+ | Phase1 | false |
+ And there is a project named "ecookbook"
+ And there is a role "manager" with the following permissions:
+ | view_work_packages |
+ | delete_work_packages |
+ | edit_cost_entries |
+ | view_cost_entries |
+ And the user "manager" is a "manager" in the project "ecookbook"
+ And there is 1 cost type with the following:
+ | name | CT |
+ And there are the following work packages in project "ecookbook":
+ | subject | author |
+ | wp1 | manager |
+ | wp2 | manager |
+ And the work package "wp1" has 1 cost entry with the following:
+ | user | manager |
+ | units | 1 |
+ | cost type | CT |
+ And I am already logged in as "manager"
+
+ @javascript
+ Scenario: Deleting a work package via the action menu
+
+ When I go to the page of the work package "wp1"
+ And I select "Delete" from the action menu
+ And I confirm popups
+
+ Then I should see "There are additional objects assossociated with the work package"
+
+ When I choose "Reassign"
+ And I fill in the id of work package "wp2" into "to_do_reassign_to_id"
+ And I submit the form by the "Delete" button
+
+ Then I should be on the work packages index page of the project called "ecookbook"
+
+ When I go to the page of the work package "wp2"
+
+ Then the work package should be shown with the following values:
+ | Spent units | 1 CTs |
diff --git a/vendored-plugins/openproject-costs/frontend/app/openproject-costs-app.js b/vendored-plugins/openproject-costs/frontend/app/openproject-costs-app.js
new file mode 100644
index 0000000000..36cc9ba86e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/openproject-costs-app.js
@@ -0,0 +1,102 @@
+//-- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+// load all js locales
+var localeFiles = require.context('../../config/locales', false, /js-[\w|-]{2,5}\.yml$/);
+localeFiles.keys().forEach(function(localeFile) {
+ var locale = localeFile.match(/js-([\w|-]{2,5})\.yml/)[1];
+ I18n.addTranslations(locale, localeFiles(localeFile)[locale]);
+});
+
+// main app
+var openprojectCostsApp = angular.module('openproject');
+
+openprojectCostsApp.run(['HookService',
+ 'ConfigurationService',
+ 'WorkPackagesOverviewService',
+ function(HookService, ConfigurationService, WorkPackagesOverviewService) {
+ var addAttributesToGroup = function(group, attributes) {
+ angular.forEach(attributes, function(id, attribute) {
+ WorkPackagesOverviewService.addAttributeToGroup(group, id || attribute);
+ });
+ };
+
+ var setupCostsAttributes = function() {
+ var position = WorkPackagesOverviewService.getGroupedWorkPackageOverviewAttributes().length - 1;
+ var costsAttributes = {
+ costObject: null,
+ overallCosts: null,
+ costsByType: null,
+ };
+
+ WorkPackagesOverviewService.addGroup('costs', position);
+
+ addAttributesToGroup('costs', costsAttributes);
+ };
+
+ if (ConfigurationService.isModuleEnabled('costs_module')) {
+ setupCostsAttributes();
+ }
+
+ HookService.register('workPackageAttributeEditableType', function(params) {
+ switch (params.type) {
+ case 'Budget':
+ return 'drop-down';
+ }
+ return null;
+ });
+
+ HookService.register('workPackageOverviewAttributes', function(params) {
+ var directive;
+ switch (params.type) {
+ case "Collection":
+ if (params.field !== 'costsByType') {
+ break;
+ }
+ directive = "summarized-cost-entries";
+ break;
+ case "Budget":
+ directive = "cost-object";
+ break;
+ }
+
+ return directive;
+ });
+
+ HookService.register('workPackageDetailsMoreMenu', function(params) {
+ return [ { key: 'log_costs', resource: 'workPackage', link: 'log_costs', css: ["icon-projects"] } ];
+ });
+}]);
+
+var requireTemplate = require.context('./templates', true, /\.html$/);
+requireTemplate.keys().forEach(requireTemplate);
+
+require('./services/cost-type-service');
+require('./work_packages/directives/cost-object-directive');
+require('./work_packages/directives/summarized-cost-entries-directive');
+require('./work_packages/directives/cost-entry-directive');
diff --git a/vendored-plugins/openproject-costs/frontend/app/services/cost-entry-service.js b/vendored-plugins/openproject-costs/frontend/app/services/cost-entry-service.js
new file mode 100644
index 0000000000..b133269630
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/services/cost-entry-service.js
@@ -0,0 +1,12 @@
+angular.module('openproject.services')
+
+.service('costEntryService', ['HALAPIResource', function(HALAPIResource) {
+ var CostEntryService = {
+ getCostEntry: function(url) {
+ var resource = HALAPIResource.setup(url);
+ return resource.fetch();
+ }
+ };
+
+ return CostEntryService;
+}]);
diff --git a/vendored-plugins/openproject-costs/frontend/app/services/cost-type-service.js b/vendored-plugins/openproject-costs/frontend/app/services/cost-type-service.js
new file mode 100644
index 0000000000..b36bb08063
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/services/cost-type-service.js
@@ -0,0 +1,12 @@
+angular.module('openproject.services')
+
+.service('CostTypeService', ['HALAPIResource', function(HALAPIResource) {
+ var CostTypeService = {
+ getCostType: function(url) {
+ var resource = HALAPIResource.setup(url, { fullyQualified: true });
+ return resource.fetch();
+ }
+ };
+
+ return CostTypeService;
+}]);
diff --git a/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_entry.html b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_entry.html
new file mode 100644
index 0000000000..a7deaabd2a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_entry.html
@@ -0,0 +1,3 @@
+
+ {{ spentUnits }} {{ unit }}
+
diff --git a/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_object.html b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_object.html
new file mode 100644
index 0000000000..016f8b1cc4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/cost_object.html
@@ -0,0 +1,6 @@
+
+
+ {{ costObject.props.subject }}
+
+
+-
diff --git a/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/summarized_cost_entries.html b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/summarized_cost_entries.html
new file mode 100644
index 0000000000..c54a59ecd9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/templates/plugin-costs/work_packages/summarized_cost_entries.html
@@ -0,0 +1,7 @@
+
+
+
+ ,
+
+
+-
diff --git a/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-entry-directive.js b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-entry-directive.js
new file mode 100644
index 0000000000..2791e38e20
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-entry-directive.js
@@ -0,0 +1,72 @@
+//-- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+angular.module('openproject.workPackages.directives')
+
+.directive('costEntry', ['PathHelper', 'CostTypeService', function(PathHelper, CostTypeService) {
+ return {
+ restrict: 'E',
+ trasclude: true,
+ templateUrl: '/templates/plugin-costs/work_packages/cost_entry.html',
+ scope: {
+ workPackage: "=",
+ costEntry: "="
+ },
+ controller: function($scope) {
+ $scope.spentUnits = $scope.costEntry.props.spentUnits;
+
+ CostTypeService.getCostType($scope.costEntry.links.costType.props.href)
+ .then(function(costType) {
+
+ $scope.costType = costType;
+
+ setUnitName();
+
+ setLink();
+ });
+
+ var setUnitName = function() {
+ if ($scope.spentUnits === "1") {
+ $scope.unit = $scope.costType.props.unit;
+ }
+ else {
+ $scope.unit = $scope.costType.props.unitPlural;
+ }
+ };
+
+ var setLink = function() {
+ var link = PathHelper.staticWorkPackagePath($scope.workPackage.props.id);
+
+ link += '/cost_entries?cost_type_id=' + $scope.costType.props.id;
+ link += '&project_id=' + $scope.workPackage.embedded.project.props.id;
+
+ $scope.summaryLink = link;
+ };
+ }
+ };
+}]);
diff --git a/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-object-directive.js b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-object-directive.js
new file mode 100644
index 0000000000..a3e61dbc61
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/cost-object-directive.js
@@ -0,0 +1,54 @@
+//-- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+angular.module('openproject.workPackages.directives')
+
+.directive('costObject', ['$timeout', function($timeout) {
+ return {
+ restrict: 'E',
+ require: '^workPackageField',
+ templateUrl: '/templates/plugin-costs/work_packages/cost_object.html',
+ link: function(scope, element, attributes, fieldController) {
+ scope.$watch(function() {
+ return fieldController.state.workPackage;
+ }, function(workPackage) {
+ scope.workPackage = workPackage;
+ scope.costObject = scope.workPackage.embedded.costObject;
+ if (scope.costObject) {
+ scope.linkToCostObject = '/cost_objects/' + scope.costObject.props.id;
+ }
+ $timeout(function() {
+ element.find('a').on('click', function(e) {
+ e.stopPropagation();
+ });
+ });
+ });
+
+ }
+ };
+}]);
diff --git a/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/summarized-cost-entries-directive.js b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/summarized-cost-entries-directive.js
new file mode 100644
index 0000000000..6e862f74d2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/frontend/app/work_packages/directives/summarized-cost-entries-directive.js
@@ -0,0 +1,46 @@
+//-- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+angular.module('openproject.workPackages.directives')
+
+.directive('summarizedCostEntries', function() {
+ return {
+ restrict: 'E',
+ require: '^workPackageField',
+ templateUrl: '/templates/plugin-costs/work_packages/summarized_cost_entries.html',
+ link: function(scope, element, attributes, fieldController) {
+ scope.workPackage = scope.field.resource;
+ scope.costTypes = scope
+ .workPackage
+ .embedded
+ .costsByType
+ .embedded
+ .elements;
+ }
+ };
+});
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_collection_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_collection_representer.rb
new file mode 100644
index 0000000000..8d32764291
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_collection_representer.rb
@@ -0,0 +1,38 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module Budgets
+ class BudgetCollectionRepresenter < ::API::Decorators::Collection
+ element_decorator ::API::V3::Budgets::BudgetRepresenter
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_representer.rb
new file mode 100644
index 0000000000..7a7908666e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budget_representer.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'roar/decorator'
+require 'roar/json/hal'
+
+module API
+ module V3
+ module Budgets
+ class BudgetRepresenter < ::API::Decorators::Single
+ self_link title_getter: -> (*) { represented.subject }
+ property :id, render_nil: true
+ property :subject, render_nil: true
+
+ private
+
+ def _type
+ 'Budget'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_api.rb b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_api.rb
new file mode 100644
index 0000000000..6bf2572102
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_api.rb
@@ -0,0 +1,50 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module Budgets
+ class BudgetsAPI < ::API::OpenProjectAPI
+ resources :budgets do
+ route_param :id do
+ before do
+ @budget = CostObject.find(params[:id])
+
+ authorize_any([:view_work_packages, :view_budgets], projects: @budget.project)
+ end
+
+ get do
+ BudgetRepresenter.new(@budget, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_by_project_api.rb b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_by_project_api.rb
new file mode 100644
index 0000000000..b82088c144
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/budgets/budgets_by_project_api.rb
@@ -0,0 +1,50 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module Budgets
+ class BudgetsByProjectAPI < ::API::OpenProjectAPI
+ resources :budgets do
+ before do
+ authorize_any([:view_work_packages, :view_budgets], projects: @project)
+ @budgets = @project.cost_objects
+ end
+
+ get do
+ BudgetCollectionRepresenter.new(@budgets,
+ @budgets.count,
+ api_v3_paths.budgets_by_project(@project.id),
+ current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb
new file mode 100644
index 0000000000..97d42020d1
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/aggregated_cost_entry_representer.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module API
+ module V3
+ module CostEntries
+ # N.B. This class is currently quite specifically crafted for the aggregation of cost entries
+ # of a single work package by their type. This might be improved in the future™
+ class AggregatedCostEntryRepresenter < ::API::Decorators::Single
+ def initialize(cost_type, units)
+ @cost_type = cost_type
+ @spent_units = units
+
+ super(nil, current_user: nil)
+ end
+
+ linked_property :cost_type,
+ getter: -> { @cost_type },
+ embed_as: ::API::V3::CostTypes::CostTypeRepresenter
+
+ property :spent_units,
+ exec_context: :decorator,
+ getter: -> (*) { @spent_units }
+
+ private
+
+ def _type
+ 'AggregatedCostEntry'
+ end
+
+ def model_required?
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_api.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_api.rb
new file mode 100644
index 0000000000..048a0181bc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_api.rb
@@ -0,0 +1,58 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostEntries
+ class CostEntriesAPI < ::API::OpenProjectAPI
+ resources :cost_entries do
+ route_param :id do
+ before do
+ @cost_entry = CostEntry.find(params[:id])
+
+ authorize(:view_cost_entries, context: @cost_entry.project) do
+ if current_user == @cost_entry.user
+ authorize(:view_own_cost_entries, context: @cost_entry.project)
+ else
+ raise API::Errors::Unauthorized
+ end
+ end
+ end
+
+ get do
+ CostEntryRepresenter.new(@cost_entry, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb
new file mode 100644
index 0000000000..720789af84
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entries_by_work_package_api.rb
@@ -0,0 +1,61 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostEntries
+ class CostEntriesByWorkPackageAPI < ::API::OpenProjectAPI
+ before do
+ authorize_any([:view_cost_entries, :view_own_cost_entries],
+ projects: @work_package.project)
+ @cost_helper = ::OpenProject::Costs::AttributesHelper.new(@work_package, current_user)
+ end
+
+ resources :cost_entries do
+ get do
+ path = api_v3_paths.cost_entries_by_work_package(@work_package.id)
+ cost_entries = @cost_helper.cost_entries
+ CostEntryCollectionRepresenter.new(cost_entries,
+ cost_entries.count,
+ path,
+ current_user: current_user)
+ end
+ end
+
+ resources :summarized_costs_by_type do
+ get do
+ WorkPackageCostsByTypeRepresenter.new(@work_package, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_collection_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_collection_representer.rb
new file mode 100644
index 0000000000..b3d29c4c4e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_collection_representer.rb
@@ -0,0 +1,38 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module CostEntries
+ class CostEntryCollectionRepresenter < ::API::Decorators::Collection
+ element_decorator ::API::V3::CostEntries::CostEntryRepresenter
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_representer.rb
new file mode 100644
index 0000000000..a48d664a38
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/cost_entry_representer.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module API
+ module V3
+ module CostEntries
+ class CostEntryRepresenter < ::API::Decorators::Single
+ self_link title_getter: -> (*) { nil }
+ linked_property :project, embed_as: ::API::V3::Projects::ProjectRepresenter
+ linked_property :user, embed_as: ::API::V3::Users::UserRepresenter
+ linked_property :cost_type, embed_as: ::API::V3::CostTypes::CostTypeRepresenter
+
+ # for now not embedded, because work packages are quite large
+ linked_property :work_package, title_getter: -> (*) { represented.work_package.subject }
+
+ property :id, render_nil: true
+ property :units, as: :spentUnits
+ property :spent_on,
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_date(represented.spent_on) }
+ property :created_on,
+ as: 'createdAt',
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_datetime(represented.created_on) }
+ property :updated_on,
+ as: 'updatedAt',
+ exec_context: :decorator,
+ getter: -> (*) { datetime_formatter.format_datetime(represented.updated_on) }
+
+ def _type
+ 'CostEntry'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb
new file mode 100644
index 0000000000..6f0173a694
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_entries/work_package_costs_by_type_representer.rb
@@ -0,0 +1,71 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module API
+ module V3
+ module CostEntries
+ # Ripped from ::API::Decorators::Collection, which does not support injecting
+ # decorators directly
+ # This class should use the Collection class directly (or inherit from it) in the future
+ class WorkPackageCostsByTypeRepresenter < ::API::Decorators::Single
+ link :self do
+ { href: api_v3_paths.summarized_work_package_costs_by_type(represented.id) }
+ end
+
+ property :total,
+ exec_context: :decorator,
+ getter: -> (*) { cost_helper.summarized_cost_entries.size }
+ property :count,
+ exec_context: :decorator,
+ getter: -> (*) { cost_helper.summarized_cost_entries.size }
+
+ collection :elements,
+ getter: -> (*) {
+ cost_helper.summarized_cost_entries.map { |kvp|
+ type = kvp[0]
+ units = kvp[1]
+ ::API::V3::CostEntries::AggregatedCostEntryRepresenter.new(type, units)
+ }
+ },
+ exec_context: :decorator,
+ embedded: true
+
+ private
+
+ def cost_helper
+ @cost_helper ||= ::OpenProject::Costs::AttributesHelper.new(represented, current_user)
+ end
+
+ def _type
+ 'Collection'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_type_representer.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_type_representer.rb
new file mode 100644
index 0000000000..e76644e4b6
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_type_representer.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module API
+ module V3
+ module CostTypes
+ class CostTypeRepresenter < ::API::Decorators::Single
+ self_link
+ property :id, render_nil: true
+ property :name, render_nil: true
+ property :unit,
+ render_nil: true
+ property :unit_plural,
+ render_nil: true
+ property :is_default,
+ getter: -> (*) { default }
+
+ def _type
+ 'CostType'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_types_api.rb b/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_types_api.rb
new file mode 100644
index 0000000000..3cbf784980
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/api/v3/cost_types/cost_types_api.rb
@@ -0,0 +1,56 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'api/v3/cost_types/cost_type_representer'
+
+module API
+ module V3
+ module CostTypes
+ class CostTypesAPI < ::API::OpenProjectAPI
+ resources :cost_types do
+ before do
+ authorize_any([:view_cost_entries, :view_own_cost_entries],
+ global: true,
+ user: current_user)
+ end
+
+ route_param :id do
+ before do
+ @cost_type = CostType.active.find(params[:id])
+ end
+
+ get do
+ CostTypeRepresenter.new(@cost_type, current_user: current_user)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs.rb b/vendored-plugins/openproject-costs/lib/open_project/costs.rb
new file mode 100644
index 0000000000..4a36d1f73d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module Costs
+ require 'open_project/costs/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/attributes_helper.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/attributes_helper.rb
new file mode 100644
index 0000000000..c0feeba123
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/attributes_helper.rb
@@ -0,0 +1,72 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs
+ class AttributesHelper
+ def initialize(work_package, user = User.current)
+ @work_package = work_package
+ @user = user
+ end
+
+ def overall_costs
+ @overall_costs ||= compute_overall_costs
+ end
+
+ def summarized_cost_entries
+ @summarized_cost_entries ||= cost_entries.group(:cost_type).calculate(:sum, :units)
+ end
+
+ def time_entries
+ @work_package.time_entries.visible(@user, @work_package.project)
+ end
+
+ def cost_entries
+ @cost_entries ||= @work_package.cost_entries.visible(@user, @work_package.project)
+ end
+
+ private
+
+ def compute_overall_costs
+ if material_costs || labor_costs
+ sum_costs = 0
+ sum_costs += material_costs if material_costs
+ sum_costs += labor_costs if labor_costs
+ else
+ sum_costs = nil
+ end
+ sum_costs
+ end
+
+ def material_costs
+ cost_entries_with_rate = cost_entries.select { |c| c.costs_visible_by?(@user) }
+ cost_entries_with_rate.blank? ? nil : cost_entries_with_rate.map(&:real_costs).sum
+ end
+
+ def labor_costs
+ time_entries_with_rate = time_entries.select { |c| c.costs_visible_by?(@user) }
+ time_entries_with_rate.blank? ? nil : time_entries_with_rate.map(&:real_costs).sum
+ end
+
+ def user_allowed_to?(*privileges)
+ privileges.inject(false) do |result, privilege|
+ result || @user.allowed_to?(privilege, @work_package.project)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/default_data.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/default_data.rb
new file mode 100644
index 0000000000..d43d6f9ec3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/default_data.rb
@@ -0,0 +1,32 @@
+module OpenProject
+ module Costs
+ module DefaultData
+ module_function
+
+ def load!
+ add_member_permissions!
+ end
+
+ def add_member_permissions!
+ role = member_role or raise 'Member role not found'
+
+ role.add_permission! *member_permissions
+ end
+
+ def member_role
+ Role.find_by name: I18n.t(:default_role_member)
+ end
+
+ def member_permissions
+ [
+ :view_own_hourly_rate,
+ :view_cost_rates,
+ :log_own_costs,
+ :edit_own_cost_entries,
+ :view_cost_objects,
+ :view_own_cost_entries
+ ]
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/deleted_user_fallback.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/deleted_user_fallback.rb
new file mode 100644
index 0000000000..23bf6eb6d5
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/deleted_user_fallback.rb
@@ -0,0 +1,42 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs
+ module DeletedUserFallback
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ alias_method_chain :user, :deleted_user_fallback
+ end
+ end
+
+ module InstanceMethods
+ def user_with_deleted_user_fallback(force_reload = true)
+ associated_user = user_without_deleted_user_fallback(force_reload)
+
+ if associated_user.nil? && read_attribute(:user_id).present?
+ associated_user = DeletedUser.first
+ end
+
+ associated_user
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/engine.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/engine.rb
new file mode 100644
index 0000000000..cdc6e0b4d8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/engine.rb
@@ -0,0 +1,295 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/plugins'
+require 'open_project/costs/version'
+
+module OpenProject::Costs
+ class Engine < ::Rails::Engine
+ engine_name :openproject_costs
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-costs',
+ author_url: 'http://finn.de',
+ requires_openproject: "= #{OpenProject::Costs::VERSION}",
+ settings: {
+ default: { 'costs_currency' => 'EUR','costs_currency_format' => '%n %u' },
+ partial: 'settings/openproject_costs'
+ },
+ name: 'OpenProject Costs' do
+
+ project_module :costs_module do
+ permission :view_own_hourly_rate, {}
+ permission :view_hourly_rates, {}
+
+ permission :edit_own_hourly_rate, { hourly_rates: [:set_rate, :edit, :update] },
+ require: :member
+ permission :edit_hourly_rates, { hourly_rates: [:set_rate, :edit, :update] },
+ require: :member
+ permission :view_cost_rates, {} # cost item values
+
+ permission :log_own_costs, { costlog: [:new, :create] },
+ require: :loggedin
+ permission :log_costs, { costlog: [:new, :create] },
+ require: :member
+
+ permission :edit_own_cost_entries, { costlog: [:edit, :update, :destroy] },
+ require: :loggedin
+ permission :edit_cost_entries, { costlog: [:edit, :update, :destroy] },
+ require: :member
+
+ permission :view_cost_objects, { cost_objects: [:index, :show] }
+
+ permission :view_cost_entries, { cost_objects: [:index, :show], costlog: [:index] }
+ permission :view_own_cost_entries, { cost_objects: [:index, :show], costlog: [:index] }
+
+ permission :edit_cost_objects, { cost_objects: [:index, :show, :edit, :update, :destroy, :new, :create, :copy] }
+ end
+
+ # register additional permissions for the time log
+ project_module :time_tracking do
+ permission :view_own_time_entries, { timelog: [:index, :report] }
+ end
+
+ # Menu extensions
+ menu :admin_menu,
+ :cost_types,
+ { controller: '/cost_types', action: 'index' },
+ html: { class: 'icon2 icon-types' },
+ caption: :label_cost_type_plural
+
+ menu :project_menu,
+ :cost_objects,
+ { controller: '/cost_objects', action: 'index' },
+ param: :project_id,
+ before: :settings,
+ caption: :cost_objects_title,
+ html: { class: 'icon2 icon-budget' }
+
+ Redmine::Activity.map do |activity|
+ activity.register :cost_objects, class_name: 'Activity::CostObjectActivityProvider', default: false
+ end
+ end
+
+ patches [:WorkPackage, :Project, :Query, :User, :TimeEntry, :PermittedParams,
+ :ProjectsController, :ApplicationHelper, :UsersHelper]
+ patch_with_namespace :API, :V3, :WorkPackages, :Schema, :SpecificWorkPackageSchema
+ patch_with_namespace :BasicData, :RoleSeeder
+ patch_with_namespace :BasicData, :SettingSeeder
+
+ add_api_attribute on: :work_package, ar_name: :cost_object_id, api_name: :cost_object
+
+ add_api_path :cost_entry do |id|
+ "#{root}/cost_entries/#{id}"
+ end
+
+ add_api_path :cost_entries_by_work_package do |id|
+ "#{work_package(id)}/cost_entries"
+ end
+
+ add_api_path :summarized_work_package_costs_by_type do |id|
+ "#{work_package(id)}/summarized_costs_by_type"
+ end
+
+ add_api_path :cost_type do |id|
+ "#{root}/cost_types/#{id}"
+ end
+
+ add_api_path :budget do |id|
+ "#{root}/budgets/#{id}"
+ end
+
+ add_api_path :budgets_by_project do |project_id|
+ "#{project(project_id)}/budgets"
+ end
+
+ add_api_endpoint 'API::V3::Root' do
+ mount ::API::V3::Budgets::BudgetsAPI
+ mount ::API::V3::CostEntries::CostEntriesAPI
+ mount ::API::V3::CostTypes::CostTypesAPI
+ end
+
+ add_api_endpoint 'API::V3::Projects::ProjectsAPI', :id do
+ mount ::API::V3::Budgets::BudgetsByProjectAPI
+ end
+
+ add_api_endpoint 'API::V3::WorkPackages::WorkPackagesAPI', :id do
+ mount ::API::V3::CostEntries::CostEntriesByWorkPackageAPI
+ end
+
+ extend_api_response(:v3, :work_packages, :work_package) do
+ include Redmine::I18n
+ include ActionView::Helpers::NumberHelper
+
+ link :log_costs do
+ {
+ href: new_work_packages_cost_entry_path(represented),
+ type: 'text/html',
+ title: "Log costs on #{represented.subject}"
+ } if represented.costs_enabled? && current_user_allowed_to(:log_costs, context: represented.project)
+ end
+
+ link :timeEntries do
+ {
+ href: work_package_time_entries_path(represented.id),
+ type: 'text/html',
+ title: 'Time entries'
+ } if user_has_time_entry_permissions?
+ end
+
+ linked_property :cost_object,
+ path: :budget,
+ title_getter: -> (*) { represented.cost_object.subject },
+ embed_as: ::API::V3::Budgets::BudgetRepresenter,
+ show_if: -> (*) { represented.costs_enabled? }
+
+ property :overall_costs,
+ exec_context: :decorator,
+ if: -> (*) { represented.costs_enabled? }
+
+ linked_property :costs_by_type,
+ title_getter: -> (*) { nil },
+ getter: -> (*) { represented },
+ path: :summarized_work_package_costs_by_type,
+ embed_as: ::API::V3::CostEntries::WorkPackageCostsByTypeRepresenter,
+ show_if: -> (*) {
+ represented.costs_enabled? &&
+ (current_user_allowed_to(:view_cost_entries, context: represented.project) ||
+ current_user_allowed_to(:view_own_cost_entries, context: represented.project))
+ }
+
+ property :spent_time,
+ getter: -> (*) do
+ formatter = API::V3::Utilities::DateTimeFormatter
+ formatter.format_duration_from_hours(represented.spent_hours)
+ end,
+ writeable: false,
+ exec_context: :decorator,
+ if: -> (_) { user_has_time_entry_permissions? }
+
+ send(:define_method, :overall_costs) do
+ number_to_currency(attributes_helper.overall_costs)
+ end
+
+ send(:define_method, :attributes_helper) do
+ @attributes_helper ||= OpenProject::Costs::AttributesHelper.new(represented)
+ end
+
+ send(:define_method, :cost_object) do
+ represented.cost_object
+ end
+
+ send(:define_method, :user_has_time_entry_permissions?) do
+ current_user_allowed_to(:view_time_entries, context: represented.project) ||
+ (current_user_allowed_to(:view_own_time_entries, context: represented.project) && represented.costs_enabled?)
+ end
+ end
+
+ extend_api_response(:v3, :work_packages, :work_package_attribute_links) do
+ linked_property :cost_object,
+ path: :budget,
+ namespace: :budgets,
+ show_if: -> (*) { represented.costs_enabled? }
+ end
+
+ extend_api_response(:v3, :work_packages, :schema, :work_package_schema) do
+ schema :spent_time,
+ type: 'Duration',
+ writable: false,
+ show_if: -> (*) {
+ current_user_allowed_to(:view_time_entries, context: represented.project) ||
+ (current_user_allowed_to(:view_own_time_entries, context: represented.project) &&
+ represented.project.costs_enabled?)
+ },
+ required: false
+
+ # N.B. in the long term we should have a type like "Currency", but that requires a proper
+ # format and not a string like "10 EUR"
+ schema :overall_costs,
+ type: 'String',
+ required: false,
+ writable: false,
+ show_if: -> (*) { represented.project.costs_enabled? }
+
+ schema :costs_by_type,
+ type: 'Collection',
+ name_source: :spent_units,
+ required: false,
+ writable: false,
+ show_if: -> (*) {
+ represented.project.costs_enabled? &&
+ (current_user_allowed_to(:view_cost_entries, context: represented.project) ||
+ current_user_allowed_to(:view_own_cost_entries, context: represented.project))
+ }
+
+ schema_with_allowed_collection :cost_object,
+ type: 'Budget',
+ required: false,
+ value_representer: ::API::V3::Budgets::BudgetRepresenter,
+ link_factory: -> (budget) {
+ {
+ href: api_v3_paths.budget(budget.id),
+ title: budget.subject
+ }
+ },
+ show_if: -> (*) {
+ represented.project.costs_enabled?
+ }
+ end
+
+ assets %w(costs/costs.css
+ costs/costs.js
+ work_packages/cost_object.html
+ work_packages/summarized_cost_entries.html)
+
+ initializer 'costs.register_hooks' do
+ require 'open_project/costs/hooks'
+ require 'open_project/costs/hooks/activity_hook'
+ require 'open_project/costs/hooks/work_package_hook'
+ require 'open_project/costs/hooks/project_hook'
+ require 'open_project/costs/hooks/work_package_action_menu'
+ require 'open_project/costs/hooks/work_packages_show_attributes'
+ end
+
+ initializer 'costs.register_observers' do |_app|
+ # Observers
+ ActiveRecord::Base.observers.push :rate_observer, :default_hourly_rate_observer, :costs_work_package_observer
+ end
+
+ initializer 'costs.patch_number_helper' do |_app|
+ # we have to do the patching in the initializer to make sure we only do this once in development
+ # since the NumberHelper is not unloaded
+ ActionView::Helpers::NumberHelper.send(:include, OpenProject::Costs::Patches::NumberHelperPatch)
+ end
+
+ config.to_prepare do
+ # loading the class so that acts_as_journalized gets registered
+ VariableCostObject
+
+ # TODO: this recreates the original behaviour
+ # however, it might not be desirable to allow assigning of cost_object regardless of the permissions
+ PermittedParams.permit(:new_work_package, :cost_object_id)
+ end
+
+ config.to_prepare do |_app|
+ NonStupidDigestAssets.whitelist << /work_packages\/.*\.html/
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks.rb
new file mode 100644
index 0000000000..f4d6d0d564
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Hooks
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/activity_hook.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/activity_hook.rb
new file mode 100644
index 0000000000..9f65ad1895
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/activity_hook.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class OpenProject::Costs::Hooks::ActivityHook < Redmine::Hook::ViewListener
+ render_on :activity_index_head,
+ partial: 'hooks/costs/activity_index_head'
+
+ render_on :users_show_head,
+ partial: 'hooks/costs/activity_index_head'
+
+ render_on :search_index_head,
+ partial: 'hooks/costs/activity_index_head'
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/project_hook.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/project_hook.rb
new file mode 100644
index 0000000000..cbae3d8bf6
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/project_hook.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class OpenProject::Costs::Hooks::ProjectHook < Redmine::Hook::ViewListener
+ # Renders up to two additional table headers to the membership setting
+ #
+ # Context:
+ # * :project => Current project
+ #
+ def view_projects_settings_members_table_header(context = {})
+ return unless context[:project] && context[:project].module_enabled?(:costs_module)
+
+ result = ''
+ user = User.current
+ project = context[:project]
+
+ result += content_tag(:th, User.human_attribute_name(:current_rate)) if user.allowed_to?(:view_hourly_rates, project)
+ result += content_tag(:th, l(:caption_set_rate)) if user.allowed_to?(:edit_hourly_rates, project)
+
+ result
+ end
+
+ # Renders an AJAX form to update the member's billing rate
+ # Context:
+ # * :project => Current project
+ # * :member => Current Member record
+ render_on :view_projects_settings_members_table_row, partial: 'hooks/costs/view_projects_settings_members_table_row'
+
+ # Renders table headers to update the member's billing rate
+ # Context:
+ # * :project => Current project
+ render_on :view_projects_settings_members_table_header, partial: 'hooks/costs/view_projects_settings_members_table_header'
+
+ render_on :view_projects_settings_members_table_colgroup, partial: 'hooks/costs/view_projects_settings_members_table_colgroup'
+ # TODO: implement model_project_copy_before_save
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_action_menu.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_action_menu.rb
new file mode 100644
index 0000000000..25f870d5cb
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_action_menu.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# Hooks to attach to the OpenProject action menu.
+class OpenProject::Costs::Hooks::WorkPackageActionMenuHook < Redmine::Hook::ViewListener
+ render_on :view_work_package_show_action_menu, partial: 'hooks/costs/view_work_package_show_action_menu'
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_hook.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_hook.rb
new file mode 100644
index 0000000000..9d62a3b2ee
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_package_hook.rb
@@ -0,0 +1,97 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# Hooks to attach to the Redmine WorkPackages.
+class OpenProject::Costs::Hooks::WorkPackageHook < Redmine::Hook::ViewListener
+ # Renders the Cost Object subject and basic costs information
+ # render_on :view_work_packages_show_details_bottom, :partial => 'hooks/costs/view_work_packages_show_details_bottom'
+
+ # Renders a select tag with all the Cost Objects for the bulk edit page
+ render_on :view_work_packages_bulk_edit_details_bottom, partial: 'hooks/costs/view_work_packages_bulk_edit_details_bottom'
+
+ render_on :view_work_packages_move_bottom, partial: 'hooks/costs/view_work_packages_move_bottom'
+
+ render_on :view_work_package_overview_attributes, partial: 'hooks/costs/view_work_package_overview_attributes'
+
+ # Updates the cost object after a move
+ #
+ # Context:
+ # * params => Request parameters
+ # * work_package => WorkPackage to move
+ # * target_project => Target of the move
+ # * copy => true, if the work_packages are copied rather than moved
+ def controller_work_packages_move_before_save(context = {})
+ # FIXME: In case of copy==true, this will break stuff if the original work_package is saved
+
+ cost_object_id = context[:params] && context[:params][:cost_object_id]
+ case cost_object_id
+ when '' # a.k.a "(No change)"
+ # cost objects HAVE to be changed if move is performed across project boundaries
+ # as the are project specific
+ context[:work_package].cost_object_id = nil unless (context[:work_package].project == context[:target_project])
+ when 'none'
+ context[:work_package].cost_object_id = nil
+ else
+ context[:work_package].cost_object_id = cost_object_id
+ end
+ end
+
+ # Saves the Cost Object assignment to the work_package
+ #
+ # Context:
+ # * :work_package => WorkPackage being saved
+ # * :params => HTML parameters
+ #
+ def controller_work_packages_bulk_edit_before_save(context = {})
+ case true
+
+ when context[:params][:cost_object_id].blank?
+ # Do nothing
+ when context[:params][:cost_object_id] == 'none'
+ # Unassign cost_object
+ context[:work_package].cost_object = nil
+ else
+ context[:work_package].cost_object = CostObject.find(context[:params][:cost_object_id])
+ end
+
+ ''
+ end
+
+ # Cost Object changes for the journal use the Cost Object subject
+ # instead of the id
+ #
+ # Context:
+ # * :detail => Detail about the journal change
+ #
+ def helper_work_packages_show_detail_after_setting(context = {})
+ # FIXME: Overwritting the caller is bad juju
+ if (context[:detail].prop_key == 'cost_object_id')
+ if context[:detail].value.to_i.to_s == context[:detail].value.to_s
+ d = CostObject.find_by_id(context[:detail].value)
+ context[:detail].value = d.subject unless d.nil? || d.subject.nil?
+ end
+
+ if context[:detail].old_value.to_i.to_s == context[:detail].old_value.to_s
+ d = CostObject.find_by_id(context[:detail].old_value)
+ context[:detail].old_value = d.subject unless d.nil? || d.subject.nil?
+ end
+ end
+ ''
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_packages_show_attributes.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_packages_show_attributes.rb
new file mode 100644
index 0000000000..69f648665a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/hooks/work_packages_show_attributes.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Hooks
+ class WorkPackagesShowHook < Redmine::Hook::ViewListener
+ include ActionView::Context
+ include WorkPackagesHelper
+
+ def work_packages_show_attributes(context = {})
+ @work_package = context[:work_package]
+ @project = context[:project]
+ attributes = context[:attributes]
+
+ return unless @project.module_enabled? :costs_module
+
+ attributes << cost_work_package_attributes
+ attributes.flatten!
+
+ attributes
+ end
+
+ private
+
+ def cost_work_package_attributes
+ attributes = []
+
+ attributes_helper = OpenProject::Costs::AttributesHelper.new(@work_package)
+
+ attributes << work_package_show_table_row(:cost_object) {
+ @work_package.cost_object ?
+ link_to_cost_object(@work_package.cost_object) :
+ empty_element_tag
+ }
+
+ attributes << work_package_show_table_row(:overall_costs) {
+ attributes_helper.overall_costs ?
+ number_to_currency(attributes_helper.overall_costs) :
+ empty_element_tag
+ }
+
+ if attributes_helper.summarized_cost_entries
+ attributes << work_package_show_table_row(:spent_units) {
+ summarized_cost_entry_links(attributes_helper.summarized_cost_entries, @work_package)
+ }
+ end
+
+ attributes
+ end
+
+ def summarized_cost_entry_links(cost_entries, work_package, create_link = true)
+ str_array = []
+ cost_entries.each do |cost_type, units|
+ txt = pluralize(units, cost_type.unit, cost_type.unit_plural)
+ if create_link
+ # TODO why does this have project_id, work_package_id and cost_type_id params?
+ str_array << link_to(txt, { controller: '/costlog',
+ action: 'index',
+ project_id: work_package.project,
+ work_package_id: work_package,
+ cost_type_id: cost_type },
+ title: cost_type.name)
+ else
+ str_array << "#{txt} "
+ end
+ end
+ str_array.join(', ').html_safe
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches.rb
new file mode 100644
index 0000000000..aa07cf10b8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/application_helper_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/application_helper_patch.rb
new file mode 100644
index 0000000000..1b9750e1fd
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/application_helper_patch.rb
@@ -0,0 +1,42 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::ApplicationHelperPatch
+ def self.included(base) # :nodoc:
+ # Same as typing in the class
+ base.class_eval do
+ def link_to_cost_object(cost_object, options = {})
+ title = nil
+ subject = nil
+ if options[:subject] == false
+ subject = "#{l(:label_cost_object)} ##{cost_object.id}"
+ title = truncate(cost_object.subject, length: 60)
+ else
+ subject = cost_object.subject
+ if options[:truncate]
+ subject = truncate(subject, length: options[:truncate])
+ end
+ end
+ s = link_to subject, cost_object_path(cost_object), class: cost_object.css_classes, title: title
+ s = "#{h cost_object.project} - " + s if options[:project]
+ s
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/number_helper_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/number_helper_patch.rb
new file mode 100644
index 0000000000..747c767ddd
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/number_helper_patch.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::NumberHelperPatch
+ def self.included(base) # :nodoc:
+ base.class_eval do
+ include InstanceMethods
+
+ alias_method_chain :number_to_currency, :l10n
+ end
+ end
+
+ module InstanceMethods
+ def number_to_currency_with_l10n(number, options = {})
+ options_with_default = { unit: ERB::Util.h(Setting.plugin_openproject_costs['costs_currency']),
+ format: ERB::Util.h(Setting.plugin_openproject_costs['costs_currency_format']),
+ delimiter: l(:currency_delimiter),
+ separator: l(:currency_separator) }.merge(options)
+
+ # FIXME: patch ruby instead of this code
+ # this circumvents the broken BigDecimal#to_f on Siemens's ruby
+ number = number.to_s if number.is_a? BigDecimal
+
+ number_to_currency_without_l10n(number, options_with_default)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/permitted_params_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/permitted_params_patch.rb
new file mode 100644
index 0000000000..7be7508808
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/permitted_params_patch.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::PermittedParamsPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+ end
+
+ module InstanceMethods
+ def cost_entry
+ params.require(:cost_entry).permit(:comments,
+ :units,
+ :overridden_costs,
+ :spent_on)
+ end
+
+ def cost_object
+ params.require(:cost_object).permit(:subject,
+ :description,
+ :fixed_date,
+ { new_material_budget_item_attributes: [:units, :cost_type_id, :comments, :budget] },
+ { new_labor_budget_item_attributes: [:hours, :user_id, :comments, :budget] },
+ { existing_material_budget_item_attributes: [:units, :cost_type_id, :comments, :budget] },
+ existing_labor_budget_item_attributes: [:hours, :user_id, :comments, :budget])
+ end
+
+ def cost_type
+ params.require(:cost_type).permit(:name,
+ :unit,
+ :unit_plural,
+ :default,
+ { new_rate_attributes: [:valid_from, :rate] },
+ existing_rate_attributes: [:valid_from, :rate])
+ end
+
+ def user_rates
+ params.require(:user).permit(new_rate_attributes: [:valid_from, :rate],
+ existing_rate_attributes: [:valid_from, :rate])
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/project_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/project_patch.rb
new file mode 100644
index 0000000000..ee6132d11c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/project_patch.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::ProjectPatch
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+ base.include(InstanceMethods)
+
+ base.class_eval do
+ has_many :cost_objects, dependent: :destroy
+ has_many :rates, class_name: 'HourlyRate'
+
+ has_many :member_groups, -> {
+ includes(:principal)
+ .where("#{Principal.table_name}.type='Group'")
+ }, class_name: 'Member'
+ has_many :groups, through: :member_groups, source: :principal
+ end
+ end
+
+ module ClassMethods
+ end
+
+ module InstanceMethods
+ def costs_enabled?
+ module_enabled?(:costs_module)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/projects_controller_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/projects_controller_patch.rb
new file mode 100644
index 0000000000..05e6193162
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/projects_controller_patch.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::ProjectsControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ before_filter :own_total_hours, only: [:show]
+ end
+ end
+
+ module InstanceMethods
+ def own_total_hours
+ if User.current.allowed_to?(:view_own_time_entries, @project)
+ cond = @project.project_condition(Setting.display_subprojects_work_packages?)
+ @total_hours = TimeEntry.visible.includes(:project).where(cond).sum(:hours).to_f
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/query_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/query_patch.rb
new file mode 100644
index 0000000000..f6eaca8cb1
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/query_patch.rb
@@ -0,0 +1,112 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::QueryPatch
+ class CurrencyQueryColumn < QueryColumn
+ include ActionView::Helpers::NumberHelper
+ alias :super_value :value
+
+ def initialize(name, options = {})
+ super
+
+ @sum_function = options[:summable]
+ self.summable = @sum_function.respond_to?(:call)
+ end
+
+ def value(work_package)
+ number_to_currency(work_package.send(name))
+ end
+
+ def real_value(work_package)
+ super_value work_package
+ end
+
+ def xls_formatter
+ :cost
+ end
+
+ def xls_value(work_package)
+ super_value work_package
+ end
+
+ def sum_of(work_packages)
+ @sum_function.call(work_packages)
+ end
+ end
+
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+
+ base.send(:include, InstanceMethods)
+
+ # Same as typing in the class
+ base.class_eval do
+ add_available_column(QueryColumn.new(:cost_object_subject))
+
+ add_available_column(CurrencyQueryColumn.new(
+ :material_costs,
+ summable: -> (work_packages) {
+ CostEntry.costs_of(work_packages: work_packages)
+ }))
+
+ add_available_column(CurrencyQueryColumn.new(
+ :labor_costs,
+ summable: -> (work_packages) {
+ TimeEntry.costs_of(work_packages: work_packages)
+ }))
+
+ add_available_column(CurrencyQueryColumn.new(
+ :overall_costs,
+ summable: -> (work_packages) {
+ labor_costs = TimeEntry.costs_of(work_packages: work_packages)
+ material_costs = CostEntry.costs_of(work_packages: work_packages)
+ labor_costs + material_costs
+ }))
+
+ Queries::WorkPackages::Filter.add_filter_type_by_field('cost_object_id', 'list_optional')
+
+ alias_method_chain :available_work_package_filters, :costs
+ end
+ end
+
+ module ClassMethods
+ end
+
+ module InstanceMethods
+ # Wrapper around the +available_filters+ to add a new Cost Object filter
+ def available_work_package_filters_with_costs
+ @available_filters = available_work_package_filters_without_costs
+
+ if project && project.module_enabled?(:costs_module)
+ openproject_costs_filters = {
+ 'cost_object_id' => {
+ type: :list_optional,
+ order: 14,
+ values: CostObject.where(project_id: project)
+ .order('subject ASC')
+ .pluck(:subject, :id)
+ },
+ }
+ else
+ openproject_costs_filters = {}
+ end
+ @available_filters.merge(openproject_costs_filters)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_patch.rb
new file mode 100644
index 0000000000..648eddb229
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_patch.rb
@@ -0,0 +1,86 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::RolePatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ # Same as typing in the class
+ base.class_eval do
+ alias_method_chain :allowed_to?, :inheritance
+ end
+ end
+
+ module InstanceMethods
+ # Return true if the user is allowed to do the specified action on project
+ # action can be:
+ # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
+ # * a permission Symbol (eg. :edit_project)
+ def allowed_to_with_inheritance?(action)
+ allowed_to_with_caching(action)
+ end
+
+ private
+
+ def allowed_to_with_caching(action)
+ @allowed_to_with_inheritance ||= {}
+
+ return @allowed_to_with_inheritance[action] if @allowed_to_with_inheritance.has_key?(action)
+
+ @allowed_to_with_inheritance[action] = allowed_to_without_caching(action)
+ end
+
+ def allowed_to_without_caching(action)
+ return true if allowed_to_without_inheritance?(action)
+
+ if action.is_a? Hash
+ # action is a parameter hash
+
+ # check the action based on the permissions of the role and all
+ # included permissions
+ allowed_inherited_actions.include? "#{action[:controller]}/#{action[:action]}"
+ else
+ # check, if the role has one of the parent permissions granted
+
+ permission = Redmine::AccessControl.permission(action)
+
+ return if permission.blank?
+
+ (permission.inherited_by + [permission]).map(&:name).detect { |parent| allowed_inherited_permissions.include? parent }
+
+ end
+ end
+
+ def allowed_inherited_permissions
+ @allowed_inherited_permissions ||= begin
+ all_permissions = allowed_permissions || []
+ (all_permissions | allowed_permissions.map { |sym|
+ p = Redmine::AccessControl.permission(sym)
+ p ? p.inherits.map(&:name) : []
+ }.flatten).uniq
+ end
+ end
+
+ def allowed_inherited_actions
+ @actions_allowed_inherited ||= begin
+ allowed_inherited_permissions.inject({}) { |actions, p| actions[p] = Redmine::AccessControl.allowed_actions(p); actions }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_seeder_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_seeder_patch.rb
new file mode 100644
index 0000000000..b465bed251
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/role_seeder_patch.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::RoleSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def seed_data!
+ super.tap do |_|
+ OpenProject::Costs::DefaultData.load!
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/setting_seeder_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/setting_seeder_patch.rb
new file mode 100644
index 0000000000..08093b810e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/setting_seeder_patch.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::SettingSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def data
+ original_data = super
+
+ unless original_data['default_projects_modules'].include? 'costs_module'
+ original_data['default_projects_modules'] << 'costs_module'
+ end
+
+ original_data
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/specific_work_package_schema_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/specific_work_package_schema_patch.rb
new file mode 100644
index 0000000000..3319c2d97c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/specific_work_package_schema_patch.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Backlogs Plugin
+#
+# Copyright (C)2013-2014 the OpenProject Foundation (OPF)
+# Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
+# Copyright (C)2010-2011 friflaj
+# Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
+# Copyright (C)2009-2010 Mark Maglana
+# Copyright (C)2009 Joe Heck, Nate Lowrie
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
+# The copyright follows:
+# Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
+# Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require_dependency 'api/v3/work_packages/schema/specific_work_package_schema'
+
+module OpenProject::Costs::Patches::SpecificWorkPackageSchemaPatch
+ def self.included(base)
+ base.class_eval do
+ prepend InstanceMethods
+ extend ClassMethods
+ end
+ end
+
+ module ClassMethods
+ end
+
+ module InstanceMethods
+ def assignable_values(property, _context)
+ if property == :cost_object
+ return project.try(:cost_objects)
+ end
+
+ super
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/time_entry_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/time_entry_patch.rb
new file mode 100644
index 0000000000..e0dfc1971d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/time_entry_patch.rb
@@ -0,0 +1,137 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# Patches Redmine's Users dynamically.
+module OpenProject::Costs::Patches::TimeEntryPatch
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+
+ base.send(:include, InstanceMethods)
+
+ # Same as typing in the class t.update_costs
+ base.class_eval do
+ belongs_to :rate, -> { where(type: ['HourlyRate', 'DefaultHourlyRate']) }, class_name: 'Rate'
+
+ scope :visible, lambda { |*args|
+ where(TimeEntry.visible_condition(args[0] || User.current, args[1]))
+ .includes(:project, :user)
+ .references(:project)
+ }
+
+ before_save :update_costs
+
+ def self.visible_condition(user, project)
+ %{ (#{Project.allowed_to_condition(user, :view_time_entries, project: project)} OR
+ (#{Project.allowed_to_condition(user, :view_own_time_entries, project: project)} AND #{TimeEntry.table_name}.user_id = #{user.id})) }
+ end
+
+ scope :visible_costs, lambda{|*args|
+ user = args.first || User.current
+ project = args[1]
+
+ view_hourly_rates = %{ (#{Project.allowed_to_condition(user, :view_hourly_rates, project: project)} OR
+ (#{Project.allowed_to_condition(user, :view_own_hourly_rate, project: project)} AND #{TimeEntry.table_name}.user_id = #{user.id})) }
+ view_time_entries = TimeEntry.visible_condition(user, project)
+
+ includes(:project, :user)
+ .where([view_time_entries, view_hourly_rates].join(' AND '))
+ }
+
+ def self.costs_of(work_packages:)
+ # N.B. Because of an AR quirks the code below uses statements like
+ # where(work_package_id: ids)
+ # You would expect to be able to simply write those as
+ # where(work_package: work_packages)
+ # However, AR (Rails 4.2) will not expand :includes + :references inside a subquery,
+ # which will render the query invalid. Therefore we manually extract the IDs in a separate (pluck) query.
+ ids = if work_packages.respond_to?(:pluck)
+ work_packages.pluck(:id)
+ else
+ Array(work_packages).map { |wp| wp.id }
+ end
+ TimeEntry.where(work_package_id: ids)
+ .joins(work_package: :project)
+ .visible_costs
+ .sum("COALESCE(#{TimeEntry.table_name}.overridden_costs,
+ #{TimeEntry.table_name}.costs)").to_f
+ end
+ end
+ end
+
+ module ClassMethods
+ def update_all(updates, conditions = nil, options = {})
+ # instead of a update_all, perform an individual update during work_package#move
+ # to trigger the update of the costs based on new rates
+ if conditions.respond_to?(:keys) && conditions.keys == [:work_package_id] && updates =~ /^project_id = ([\d]+)$/
+ project_id = $1
+ time_entries = TimeEntry.where(conditions)
+ time_entries.each do |entry|
+ entry.project_id = project_id
+ entry.save!
+ end
+ else
+ super
+ end
+ end
+ end
+
+ module InstanceMethods
+ def real_costs
+ # This methods returns the actual assigned costs of the entry
+ overridden_costs || costs || calculated_costs
+ end
+
+ def calculated_costs(rate_attr = nil)
+ rate_attr ||= current_rate
+ hours * rate_attr.rate
+ rescue
+ 0.0
+ end
+
+ def update_costs(rate_attr = nil)
+ rate_attr ||= current_rate
+ if rate_attr.nil?
+ self.costs = 0.0
+ self.rate = nil
+ return
+ end
+
+ self.costs = calculated_costs(rate_attr)
+ self.rate = rate_attr
+ end
+
+ def update_costs!(rate_attr = nil)
+ update_costs(rate_attr)
+ self.save!
+ end
+
+ def current_rate
+ user.rate_at(spent_on, project_id)
+ end
+
+ def visible_by?(usr)
+ usr.allowed_to?(:view_time_entries, project)
+ end
+
+ def costs_visible_by?(usr)
+ usr.allowed_to?(:view_hourly_rates, project) ||
+ (user_id == usr.id && usr.allowed_to?(:view_own_hourly_rate, project))
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/user_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/user_patch.rb
new file mode 100644
index 0000000000..15ee0905ac
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/user_patch.rb
@@ -0,0 +1,110 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::UserPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ has_many :rates, class_name: 'HourlyRate'
+ has_many :default_rates, class_name: 'DefaultHourlyRate'
+
+ before_save :save_rates
+ end
+ end
+
+ module InstanceMethods
+ def allowed_to_condition_with_project_id(permission, projects = nil)
+ project_condition = Project.allowed_to_condition(self, permission, project: projects)
+
+ ids = Project.where(project_condition).pluck(:id)
+
+ ids.empty? ?
+ '1=0' :
+ "(#{Project.table_name}.id in (#{ids.join(', ')}))"
+ end
+
+ def current_rate(project = nil, include_default = true)
+ rate_at(Date.today, project, include_default)
+ end
+
+ # kept for backwards compatibility
+ def rate_at(date, project = nil, include_default = true)
+ ::HourlyRate.at_date_for_user_in_project(date, id, project, include_default)
+ end
+
+ def current_default_rate
+ ::DefaultHourlyRate.at_for_user(Date.today, id)
+ end
+
+ # kept for backwards compatibility
+ def default_rate_at(date)
+ ::DefaultHourlyRate.at_for_user(date, id)
+ end
+
+ def add_rates(project, rate_attributes)
+ # set project to nil to set the default rates
+
+ return unless rate_attributes
+
+ rate_attributes.each do |_index, attributes|
+ attributes[:rate] = Rate.clean_currency(attributes[:rate])
+
+ if project.nil?
+ default_rates.build(attributes)
+ else
+ attributes[:project] = project
+ rates.build(attributes)
+ end
+ end
+ end
+
+ def set_existing_rates(project, rates_attributes)
+ if project.nil?
+ default_rates.reject(&:new_record?).each do |rate|
+ update_rate(rate, rates_attributes[rate.id.to_s], false)
+ end
+ else
+ rates.reject { |r| r.new_record? || r.project_id != project.id }.each do |rate|
+ update_rate(rate, rates_attributes[rate.id.to_s], true)
+ end
+ end
+ end
+
+ def save_rates
+ (default_rates + rates).each do |rate|
+ return false if !rate.save
+ end
+ end
+
+ private
+
+ def update_rate(rate, attributes, project_rate = true)
+ if attributes && attributes[:rate].present?
+ attributes[:rate] = Rate.clean_currency(attributes[:rate])
+ rate.attributes = attributes
+ else
+ # TODO: this is surprising
+ # as it actually deletes the rate right away
+ # as opposed to the behaviour when changing the attributes
+ project_rate ? rates.delete(rate) : default_rates.delete(rate)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/users_helper_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/users_helper_patch.rb
new file mode 100644
index 0000000000..218f83c3f4
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/users_helper_patch.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::UsersHelperPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ # Same as typing in the class
+ base.class_eval do
+ alias_method_chain :user_settings_tabs, :rate_tab
+ end
+ end
+
+ module InstanceMethods
+ # Adds a rates tab to the user administration page
+ def user_settings_tabs_with_rate_tab
+ # Core defined data
+ tabs = user_settings_tabs_without_rate_tab
+ tabs << { name: 'rates', partial: 'users/rates', label: :caption_rate_history }
+ tabs
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/patches/work_package_patch.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/work_package_patch.rb
new file mode 100644
index 0000000000..46519d53b2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/patches/work_package_patch.rb
@@ -0,0 +1,136 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Costs::Patches::WorkPackagePatch
+ def self.included(base) # :nodoc:
+ base.extend(ClassMethods)
+
+ base.send(:include, InstanceMethods)
+
+ # Same as typing in the class
+ base.class_eval do
+ belongs_to :cost_object, inverse_of: :work_packages
+ has_many :cost_entries, dependent: :delete_all
+
+ # disabled for now, implements part of ticket blocking
+ validate :validate_cost_object
+
+ register_journal_formatter(:cost_association) do |value, journable, field|
+ association = journable.class.reflect_on_association(field.to_sym)
+ if association
+ record = association.class_name.constantize.find_by_id(value.to_i)
+ record.subject if record
+ end
+ end
+
+ register_on_journal_formatter(:cost_association, 'cost_object_id')
+
+ associated_to_ask_before_destruction CostEntry,
+ ->(work_packages) { CostEntry.on_work_packages(work_packages).count > 0 },
+ method(:cleanup_cost_entries_before_destruction_of)
+ end
+ end
+
+ module ClassMethods
+ protected
+
+ def cleanup_cost_entries_before_destruction_of(work_packages, user, to_do = { action: 'destroy' })
+ work_packages = Array(work_packages)
+
+ return false unless to_do.present?
+
+ case to_do[:action]
+ when 'destroy'
+ true
+ # nothing to do
+ when 'nullify'
+ work_packages.each do |wp|
+ wp.errors.add(:base, :nullify_is_not_valid_for_cost_entries)
+ end
+
+ false
+ when 'reassign'
+ reassign_to = WorkPackage
+ .where(Project.allowed_to_condition(user, :edit_cost_entries))
+ .includes(:project)
+ .references(:projects)
+ .find_by_id(to_do[:reassign_to_id])
+
+ if reassign_to.nil?
+ work_packages.each do |wp|
+ wp.errors.add(:base, :is_not_a_valid_target_for_cost_entries, id: to_do[:reassign_to_id])
+ end
+
+ false
+ else
+ WorkPackage.update_cost_entries(work_packages.map(&:id), "work_package_id = #{reassign_to.id}, project_id = #{reassign_to.project_id}")
+ end
+ else
+ false
+ end
+ end
+
+ protected
+
+ def update_cost_entries(work_packages, action)
+ CostEntry.where(work_package_id: work_packages).update_all(action)
+ end
+ end
+
+ module InstanceMethods
+ def costs_enabled?
+ project && project.costs_enabled?
+ end
+
+ def validate_cost_object
+ if cost_object_id_changed?
+ unless cost_object_id.blank? || project.cost_object_ids.include?(cost_object_id)
+ errors.add :cost_object, :inclusion
+ end
+ end
+ end
+
+ def material_costs
+ CostEntry.costs_of(work_packages: self)
+ end
+
+ def labor_costs
+ TimeEntry.costs_of(work_packages: self)
+ end
+
+ def overall_costs
+ labor_costs + material_costs
+ end
+
+ # Wraps the association to get the Cost Object subject. Needed for the
+ # Query and filtering
+ def cost_object_subject
+ unless cost_object.nil?
+ return cost_object.subject
+ end
+ end
+
+ def update_costs!
+ # This methods ist referenced from some migrations but does nothing
+ # anymore.
+ end
+ end
+end
+
+WorkPackage::SAFE_ATTRIBUTES << 'cost_object_id' if WorkPackage.const_defined? 'SAFE_ATTRIBUTES'
diff --git a/vendored-plugins/openproject-costs/lib/open_project/costs/version.rb b/vendored-plugins/openproject-costs/lib/open_project/costs/version.rb
new file mode 100644
index 0000000000..0af8bb0332
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/open_project/costs/version.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module Costs
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-costs/lib/openproject-costs.rb b/vendored-plugins/openproject-costs/lib/openproject-costs.rb
new file mode 100644
index 0000000000..fadb80f8c3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/lib/openproject-costs.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/costs'
diff --git a/vendored-plugins/openproject-costs/openproject-costs.gemspec b/vendored-plugins/openproject-costs/openproject-costs.gemspec
new file mode 100644
index 0000000000..04bd0354b9
--- /dev/null
+++ b/vendored-plugins/openproject-costs/openproject-costs.gemspec
@@ -0,0 +1,23 @@
+$:.push File.expand_path('../lib', __FILE__)
+
+# Maintain your gem's version:
+require 'open_project/costs/version'
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = 'openproject-costs'
+ s.version = OpenProject::Costs::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/costs-plugin'
+ s.summary = 'OpenProject Costs'
+ s.description = 'This Plugin adds features for planning and tracking costs of projects.'
+ s.license = 'GPLv3'
+
+ s.files = Dir['{app,config,db,lib,doc}/**/*', 'README.md']
+ s.test_files = Dir['spec/**/*']
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_development_dependency 'factory_girl_rails', '~> 4.0'
+end
diff --git a/vendored-plugins/openproject-costs/package.json b/vendored-plugins/openproject-costs/package.json
new file mode 100644
index 0000000000..43fa7d12dc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "openproject-costs",
+ "version": "0.1.0",
+ "main": "frontend/app/openproject-costs-app.js",
+ "dependencies": {}
+}
diff --git a/vendored-plugins/openproject-costs/spec/controllers/cost_types_controller_spec.rb b/vendored-plugins/openproject-costs/spec/controllers/cost_types_controller_spec.rb
new file mode 100644
index 0000000000..6b9938e2a8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/controllers/cost_types_controller_spec.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe CostTypesController, type: :controller do
+ let(:admin) { FactoryGirl.create(:admin) }
+ let(:cost_type) { FactoryGirl.create(:cost_type) }
+
+ describe 'DELETE destroy' do
+ it 'allows an admin to delete' do
+ as_logged_in_user admin do
+ delete :destroy, id: cost_type.id
+ end
+
+ expect(assigns(:cost_type).deleted_at).to be_a Time
+ expect(response).to redirect_to cost_types_path
+ expect(flash[:notice]).to eq I18n.t(:notice_successful_lock)
+ end
+ end
+
+ describe 'PATCH restore' do
+ before do
+ cost_type.deleted_at = DateTime.now
+ end
+
+ it 'allows an admin to restore' do
+ as_logged_in_user admin do
+ patch :restore, id: cost_type.id
+ end
+
+ expect(assigns(:cost_type).deleted_at).to be_nil
+ expect(response).to redirect_to cost_types_path
+ expect(flash[:notice]).to eq I18n.t(:notice_successful_restore)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/controllers/costlog_controller_spec.rb b/vendored-plugins/openproject-costs/spec/controllers/costlog_controller_spec.rb
new file mode 100644
index 0000000000..f821092238
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/controllers/costlog_controller_spec.rb
@@ -0,0 +1,691 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe CostlogController, type: :controller do
+ include Cost::PluginSpecHelper
+ let (:project) { FactoryGirl.create(:project_with_types) }
+ let (:work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ author: user,
+ type: project.types.first)
+ }
+ let (:user) { FactoryGirl.create(:user) }
+ let (:user2) { FactoryGirl.create(:user) }
+ let (:controller) { FactoryGirl.build(:role, permissions: [:log_costs, :edit_cost_entries]) }
+ let (:cost_type) { FactoryGirl.build(:cost_type) }
+ let (:cost_entry) {
+ FactoryGirl.build(:cost_entry, work_package: work_package,
+ project: project,
+ spent_on: Date.today,
+ overridden_costs: 400,
+ units: 100,
+ user: user,
+ comments: '')
+ }
+ let(:work_package_status) { FactoryGirl.create(:work_package_status, is_default: true) }
+
+ def grant_current_user_permissions(user, permissions)
+ member = FactoryGirl.build(:member, project: project,
+ principal: user)
+ member.roles << FactoryGirl.build(:role, permissions: permissions)
+ member.principal = user
+ member.save!
+ user.reload # in order to refresh the member/membership associations
+ allow(User).to receive(:current).and_return(user)
+ end
+
+ def disable_flash_sweep
+ allow(@controller.flash).to receive(:sweep)
+ end
+
+ shared_examples_for 'assigns' do
+ it { expect(assigns(:cost_entry).project).to eq(expected_project) }
+ it { expect(assigns(:cost_entry).work_package).to eq(expected_work_package) }
+ it { expect(assigns(:cost_entry).user).to eq(expected_user) }
+ it { expect(assigns(:cost_entry).spent_on).to eq(expected_spent_on) }
+ it { expect(assigns(:cost_entry).cost_type).to eq(expected_cost_type) }
+ it { expect(assigns(:cost_entry).units).to eq(expected_units) }
+ it { expect(assigns(:cost_entry).overridden_costs).to eq(expected_overridden_costs) }
+ end
+
+ before do
+ disable_flash_sweep
+ allow(@controller).to receive(:check_if_login_required)
+ end
+
+ after do
+ User.current = nil
+ end
+
+ describe 'GET new' do
+ let(:params) { { 'work_package_id' => work_package.id.to_s } }
+
+ let(:expected_project) { project }
+ let(:expected_work_package) { work_package }
+ let(:expected_user) { user }
+ let(:expected_spent_on) { Date.today }
+ let(:expected_cost_type) { nil }
+ let(:expected_overridden_costs) { nil }
+ let(:expected_units) { nil }
+
+ shared_examples_for 'successful new' do
+ before do
+ get :new, params
+ end
+
+ it { expect(response).to be_success }
+ it_should_behave_like 'assigns'
+ it { expect(response).to render_template('edit') }
+ end
+
+ shared_examples_for 'forbidden new' do
+ before do
+ get :new, params
+ end
+
+ it { expect(response.response_code).to eq(403) }
+ end
+
+ describe 'WHEN user allowed to create new cost_entry' do
+ before do
+ grant_current_user_permissions user, [:log_costs]
+ end
+
+ it_should_behave_like 'successful new'
+ end
+
+ describe "WHEN user allowed to create new cost_entry
+ WHEN a default cost_type exists" do
+ let(:expected_cost_type) { cost_type }
+
+ before do
+ cost_type.default = true
+ cost_type.save!
+
+ grant_current_user_permissions user, [:log_costs]
+ end
+
+ it_should_behave_like 'successful new'
+ end
+
+ describe 'WHEN user is allowed to create new own cost_entry' do
+ before do
+ grant_current_user_permissions user, [:log_own_costs]
+ end
+
+ it_should_behave_like 'successful new'
+ end
+
+ describe 'WHEN user is not allowed to create new cost_entries' do
+ before do
+ grant_current_user_permissions user, []
+ end
+
+ it_should_behave_like 'forbidden new'
+ end
+ end
+
+ describe 'GET edit' do
+ let(:params) { { 'id' => cost_entry.id.to_s } }
+
+ before do
+ cost_entry.save(validate: false)
+ end
+
+ shared_examples_for 'successful edit' do
+ before do
+ get :edit, params
+ end
+
+ it { expect(response).to be_success }
+ it { expect(assigns(:cost_entry)).to eq(cost_entry) }
+ it { expect(assigns(:cost_entry)).not_to be_changed }
+ it { expect(response).to render_template('edit') }
+ end
+
+ shared_examples_for 'forbidden edit' do
+ before do
+ get :edit, params
+ end
+
+ it { expect(response.response_code).to eq(403) }
+ end
+
+ describe 'WHEN the user is allowed to edit cost_entries' do
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+ end
+
+ it_should_behave_like 'successful edit'
+ end
+
+ describe "WHEN the user is allowed to edit cost_entries
+ WHEN trying to edit a not own cost_entry" do
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ cost_entry.user = FactoryGirl.create(:user)
+ cost_entry.save(validate: false)
+ end
+
+ it_should_behave_like 'successful edit'
+ end
+
+ describe 'WHEN the user is allowed to edit own cost_entries' do
+ before do
+ grant_current_user_permissions user, [:edit_own_cost_entries]
+ end
+
+ it_should_behave_like 'successful edit'
+ end
+
+ describe "WHEN the user is allowed to edit own cost_entries
+ WHEN trying to edit a not own cost_entry" do
+ before do
+ grant_current_user_permissions user, [:edit_own_cost_entries]
+
+ cost_entry.user = FactoryGirl.create(:user)
+ cost_entry.save(validate: false)
+ end
+
+ it_should_behave_like 'forbidden edit'
+ end
+
+ describe 'WHEN the user is not allowed to edit cost_entries' do
+ before do
+ grant_current_user_permissions user, []
+ end
+
+ it_should_behave_like 'forbidden edit'
+ end
+
+ describe "WHEN the user is allowed to edit cost_entries
+ WHEN the cost_entry is associated to a different project" do
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ cost_entry.project = FactoryGirl.create(:project_with_types)
+ cost_entry.work_package = FactoryGirl.create(:work_package, project: cost_entry.project,
+ type: cost_entry.project.types.first,
+ author: user)
+ cost_entry.save!
+ end
+
+ it_should_behave_like 'forbidden edit'
+ end
+
+ describe "WHEN the user is allowed to edit cost_entries
+ WHEN the provided id is invalid" do
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['id'] = (cost_entry.id + 1).to_s
+
+ get :edit, params
+ end
+
+ it { expect(response.response_code).to eq(404) }
+ end
+ end
+
+ describe 'POST create' do
+ let (:params) {
+ { 'project_id' => project.id.to_s,
+ 'cost_entry' => { 'user_id' => user.id.to_s,
+ 'work_package_id' => (work_package.present? ? work_package.id.to_s : ''),
+ 'units' => units.to_s,
+ 'cost_type_id' => (cost_type.present? ? cost_type.id.to_s : ''),
+ 'comments' => 'lorem',
+ 'spent_on' => date.to_s,
+ 'overridden_costs' => overridden_costs.to_s } }
+ }
+ let(:expected_project) { project }
+ let(:expected_work_package) { work_package }
+ let(:expected_user) { user }
+ let(:expected_overridden_costs) { overridden_costs }
+ let(:expected_spent_on) { date }
+ let(:expected_cost_type) { cost_type }
+ let(:expected_units) { units }
+
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:date) { '2012-04-03'.to_date }
+ let(:overridden_costs) { 500.00 }
+ let(:units) { 5.0 }
+
+ before do
+ cost_type.save! if cost_type.present?
+ end
+
+ shared_examples_for 'successful create' do
+ before do
+ post :create, params
+ end
+
+ # is this really usefull, shouldn't it redirect to the creating work_package by default?
+ it { expect(response).to redirect_to(controller: 'costlog', action: 'index', project_id: project) }
+ it { expect(assigns(:cost_entry)).not_to be_new_record }
+ it_should_behave_like 'assigns'
+ it { expect(flash[:notice]).to eql('Unit cost logged successfully.') }
+ end
+
+ shared_examples_for 'invalid create' do
+ before do
+ post :create, params
+ end
+
+ it { expect(response).to be_success }
+ it_should_behave_like 'assigns'
+ it { expect(flash[:notice]).to be_nil }
+ end
+
+ shared_examples_for 'forbidden create' do
+ before do
+ post :create, params
+ end
+
+ it { expect(response.response_code).to eq(403) }
+ end
+
+ describe 'WHEN the user is allowed to create cost_entries' do
+ before do
+ grant_current_user_permissions user, [:log_costs]
+ end
+
+ it_should_behave_like 'successful create'
+ end
+
+ describe 'WHEN the user is allowed to create own cost_entries' do
+ before do
+ grant_current_user_permissions user, [:log_own_costs]
+ end
+
+ it_should_behave_like 'successful create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN no date is specified" do
+ let(:expected_spent_on) { Date.today }
+
+ before do
+ grant_current_user_permissions user, [:log_costs]
+
+ params['cost_entry'].delete('spent_on')
+ end
+
+ it_should_behave_like 'successful create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN a non existing cost_type_id is specified
+ WHEN no default cost_type is defined" do
+ let(:expected_cost_type) { nil }
+
+ before do
+ grant_current_user_permissions user, [:log_costs]
+ params['cost_entry']['cost_type_id'] = (cost_type.id + 1).to_s
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN a non existing cost_type_id is specified
+ WHEN a default cost_type is defined" do
+ let(:expected_cost_type) { nil }
+
+ before do
+ FactoryGirl.create(:cost_type, default: true)
+
+ grant_current_user_permissions user, [:log_costs]
+ params['cost_entry']['cost_type_id'] = 1
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN no cost_type is specified
+ WHEN a default cost_type is defined" do
+ let(:expected_cost_type) { nil }
+
+ before do
+ FactoryGirl.create(:cost_type, default: true)
+
+ grant_current_user_permissions user, [:log_costs]
+ params['cost_entry'].delete('cost_type_id')
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN no cost_type is specified
+ WHEN no default cost_type is defined" do
+ let(:expected_cost_type) { nil }
+
+ before do
+ grant_current_user_permissions user, [:log_costs]
+ params['cost_entry'].delete('cost_type_id')
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN the cost_type id provided belongs to an inactive cost_type" do
+ before do
+ grant_current_user_permissions user, [:log_costs]
+ cost_type.deleted_at = Date.today
+ cost_type.save!
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN the user is allowed to log cost for someone else and is doing so
+ WHEN the other user is a member of the project" do
+ before do
+ grant_current_user_permissions user, []
+ grant_current_user_permissions user2, [:log_costs]
+
+ params['cost_entry']['user_id'] = user.id.to_s
+ end
+
+ it_should_behave_like 'successful create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN the user is allowed to log cost for someone else and is doing so
+ WHEN the other user isn't a member of the project" do
+ before do
+ grant_current_user_permissions user2, [:log_costs]
+
+ params['cost_entry']['user_id'] = user.id.to_s
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN the id of an work_package not included in the provided project is provided" do
+ let(:project2) { FactoryGirl.create(:project_with_types) }
+ let(:work_package2) {
+ FactoryGirl.create(:work_package, project: project2,
+ type: project2.types.first,
+ author: user)
+ }
+ let(:expected_work_package) { work_package2 }
+
+ before do
+ grant_current_user_permissions user, [:log_costs]
+
+ params['cost_entry']['work_package_id'] = work_package2.id
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create cost_entries
+ WHEN no work_package_id is provided" do
+ let(:expected_work_package) { nil }
+
+ before do
+ grant_current_user_permissions user, [:log_costs]
+
+ params['cost_entry'].delete('work_package_id')
+ end
+
+ it_should_behave_like 'invalid create'
+ end
+
+ describe "WHEN the user is allowed to create own cost_entries
+ WHEN the user is trying to log costs for somebody else" do
+ before do
+ grant_current_user_permissions user2, [:log_own_costs]
+
+ params['cost_entry']['user_id'] = user.id
+ end
+
+ it_should_behave_like 'forbidden create'
+ end
+
+ describe 'WHEN the user is not allowed to create cost_entries' do
+ before do
+ grant_current_user_permissions user, []
+ end
+
+ it_should_behave_like 'forbidden create'
+ end
+ end
+
+ describe 'PUT update' do
+ let(:params) {
+ { 'id' => cost_entry.id.to_s,
+ 'cost_entry' => { 'comments' => 'lorem',
+ 'work_package_id' => cost_entry.work_package.id.to_s,
+ 'units' => cost_entry.units.to_s,
+ 'spent_on' => cost_entry.spent_on.to_s,
+ 'user_id' => cost_entry.user.id.to_s,
+ 'cost_type_id' => cost_entry.cost_type.id.to_s } }
+ }
+
+ before do
+ cost_entry.save(validate: false)
+ end
+
+ let(:expected_work_package) { cost_entry.work_package }
+ let(:expected_user) { cost_entry.user }
+ let(:expected_project) { cost_entry.project }
+ let(:expected_cost_type) { cost_entry.cost_type }
+ let(:expected_units) { cost_entry.units }
+ let(:expected_overridden_costs) { cost_entry.overridden_costs }
+ let(:expected_spent_on) { cost_entry.spent_on }
+
+ shared_examples_for 'successful update' do
+ before do
+ put :update, params
+ end
+
+ it { expect(response).to redirect_to(controller: 'costlog', action: 'index', project_id: project) }
+ it { expect(assigns(:cost_entry)).to eq(cost_entry) }
+ it_should_behave_like 'assigns'
+ it { expect(assigns(:cost_entry)).not_to be_changed }
+ it { expect(flash[:notice]).to eql I18n.t(:notice_successful_update) }
+ end
+
+ shared_examples_for 'invalid update' do
+ before { put :update, params }
+
+ it_should_behave_like 'assigns'
+ it { expect(response).to be_success }
+ it { expect(flash[:notice]).to be_nil }
+ end
+
+ shared_examples_for 'forbidden update' do
+ before do
+ put :update, params
+ end
+
+ it { expect(response.response_code).to eq(403) }
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating:
+ work_package_id
+ user_id
+ units
+ cost_type
+ overridden_costs
+ spent_on" do
+ let(:expected_work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ type: project.types.first,
+ author: user)
+ }
+ let(:expected_user) { FactoryGirl.create(:user) }
+ let(:expected_spent_on) { cost_entry.spent_on + 4.days }
+ let(:expected_units) { cost_entry.units + 20 }
+ let(:expected_cost_type) { FactoryGirl.create(:cost_type) }
+ let(:expected_overridden_costs) { cost_entry.overridden_costs + 300 }
+
+ before do
+ grant_current_user_permissions expected_user, []
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['work_package_id'] = expected_work_package.id.to_s
+ params['cost_entry']['user_id'] = expected_user.id.to_s
+ params['cost_entry']['spent_on'] = expected_spent_on.to_s
+ params['cost_entry']['units'] = expected_units.to_s
+ params['cost_entry']['cost_type_id'] = expected_cost_type.id.to_s
+ params['cost_entry']['overridden_costs'] = expected_overridden_costs.to_s
+ end
+
+ it_should_behave_like 'successful update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating nothing" do
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+ end
+
+ it_should_behave_like 'successful update'
+ end
+
+ describe "WHEN the user is allowed ot update own cost_entries
+ WHEN updating something" do
+ let(:expected_units) { cost_entry.units + 20 }
+
+ before do
+ grant_current_user_permissions user, [:edit_own_cost_entries]
+
+ params['cost_entry']['units'] = expected_units.to_s
+ end
+
+ it_should_behave_like 'successful update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating the user
+ WHEN the new user isn't a member of the project" do
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:expected_user) { user2 }
+
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['user_id'] = user2.id.to_s
+ end
+
+ it_should_behave_like 'invalid update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating the work_package
+ WHEN the new work_package isn't an work_package of the current project" do
+ let(:project2) { FactoryGirl.create(:project_with_types) }
+ let(:work_package2) {
+ FactoryGirl.create(:work_package, project: project2,
+ type: project2.types.first)
+ }
+ let(:expected_work_package) { work_package2 }
+
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['work_package_id'] = work_package2.id.to_s
+ end
+
+ it_should_behave_like 'invalid update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating the work_package
+ WHEN the new work_package_id isn't existing" do
+ let(:expected_work_package) { nil }
+
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['work_package_id'] = (work_package.id + 1).to_s
+ end
+
+ it_should_behave_like 'invalid update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating the cost_type
+ WHEN the new cost_type is deleted" do
+ let(:expected_cost_type) { FactoryGirl.create(:cost_type, deleted_at: Date.today) }
+
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['cost_type_id'] = expected_cost_type.id.to_s
+ end
+
+ it_should_behave_like 'invalid update'
+ end
+
+ describe "WHEN the user is allowed to update cost_entries
+ WHEN updating the cost_type
+ WHEN the new cost_type doesn't exist" do
+ let(:expected_cost_type) { nil }
+
+ before do
+ grant_current_user_permissions user, [:edit_cost_entries]
+
+ params['cost_entry']['cost_type_id'] = '1'
+ end
+
+ it_should_behave_like 'invalid update'
+ end
+
+ describe "WHEN the user is allowed to update own cost_entries and not all
+ WHEN updating own cost entry
+ WHEN updating the user" do
+ let(:user3) { FactoryGirl.create(:user) }
+
+ before do
+ grant_current_user_permissions user, [:edit_own_cost_entries]
+
+ params['cost_entry']['user_id'] = user3.id
+ end
+
+ it_should_behave_like 'forbidden update'
+ end
+
+ describe "WHEN the user is allowed to update own cost_entries and not all
+ WHEN updating foreign cost_entry
+ WHEN updating someting" do
+ let(:user3) { FactoryGirl.create(:user) }
+
+ before do
+ grant_current_user_permissions user3, [:edit_own_cost_entries]
+
+ params['cost_entry']['units'] = (cost_entry.units + 20).to_s
+ end
+
+ it_should_behave_like 'forbidden update'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/controllers/hourly_rates_controller_spec.rb b/vendored-plugins/openproject-costs/spec/controllers/hourly_rates_controller_spec.rb
new file mode 100644
index 0000000000..788b00db52
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/controllers/hourly_rates_controller_spec.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe HourlyRatesController do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:admin) { FactoryGirl.create(:admin) }
+ let(:default_rate) { FactoryGirl.create(:default_hourly_rate, user: user) }
+
+ describe 'PUT update' do
+ describe 'WHEN trying to update with an invalid rate value' do
+ let(:params) {
+ {
+ id: user.id,
+ user: { 'existing_rate_attributes' => { "#{default_rate.id}" => { 'valid_from' => "#{default_rate.valid_from}", 'rate' => '2d5' } } }
+ }
+ }
+ before do
+ as_logged_in_user admin do
+ post :update, params
+ end
+ end
+
+ it 'should render the edit template' do
+ expect(response).to render_template('edit')
+ end
+
+ it 'should display an error message' do
+ actual_message = assigns(:user).default_rates.first.errors.messages[:rate].first
+ expect(actual_message).to eq(I18n.t('activerecord.errors.messages.not_a_number'))
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/controllers/work_packages_bulk_controller_spec.rb b/vendored-plugins/openproject-costs/spec/controllers/work_packages_bulk_controller_spec.rb
new file mode 100644
index 0000000000..6ed6731c9e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/controllers/work_packages_bulk_controller_spec.rb
@@ -0,0 +1,42 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe WorkPackages::BulkController, type: :controller do
+ let(:project) { FactoryGirl.create(:project_with_types) }
+ let(:controller_role) { FactoryGirl.build(:role, permissions: [:view_work_packages, :edit_work_packages]) }
+ let(:user) { FactoryGirl.create :user, member_in_project: project, member_through_role: controller_role }
+ let(:cost_object) { FactoryGirl.create :cost_object, project: project }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+
+ before do
+ allow(User).to receive(:current).and_return user
+ end
+
+ describe '#update' do
+ context 'when a cost report is assigned' do
+ before do put :update, ids: [work_package.id], work_package: { cost_object_id: cost_object.id } end
+
+ subject { work_package.reload.cost_object.try :id }
+
+ it { is_expected.to eq(cost_object.id) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/disabled_specs.rb b/vendored-plugins/openproject-costs/spec/disabled_specs.rb
new file mode 100644
index 0000000000..e0f8e8dbff
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/disabled_specs.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'rspec/example_disabler'
+
+RSpec::ExampleDisabler.disable_example('WorkPackagesController index with valid query settings passed to front-end client visible attributes all attributes visible', 'plugin openproject-costs changes behavior')
+RSpec::ExampleDisabler.disable_example('API::V3::WorkPackages::WorkPackageRepresenter generation spentTime content time entry with multiple hours', 'plugin openproject-costs changes behavior')
+RSpec::ExampleDisabler.disable_example('API::V3::WorkPackages::WorkPackageRepresenter generation spentTime content no time entry', 'plugin openproject-costs changes behavior')
+RSpec::ExampleDisabler.disable_example('API::V3::WorkPackages::WorkPackageRepresenter generation spentTime content time entry with single hour', 'plugin openproject-costs changes behavior')
+
+RSpec::ExampleDisabler.disable_example('API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter generation spentTime not allowed to view time entries does not show spentTime', 'plugin openproject-costs causes unexpected message')
diff --git a/vendored-plugins/openproject-costs/spec/factories/cost_entry_factory.rb b/vendored-plugins/openproject-costs/spec/factories/cost_entry_factory.rb
new file mode 100644
index 0000000000..8bf077b9cb
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/cost_entry_factory.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :cost_entry do
+ project
+ user do FactoryGirl.create(:user, member_in_project: project)end
+ work_package do FactoryGirl.create(:work_package, project: project) end
+ cost_type
+ spent_on Date.today
+ units 1
+ comments ''
+ created_on do Time.now end
+ updated_on { Time.now }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/cost_object_factory.rb b/vendored-plugins/openproject-costs/spec/factories/cost_object_factory.rb
new file mode 100644
index 0000000000..fe5b5579a5
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/cost_object_factory.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :cost_object do
+ subject 'Some Cost Object'
+ description 'Some costs'
+ kind 'VariableCostObject'
+ project
+ fixed_date Date.today
+ created_on 3.days.ago
+ updated_on 3.days.ago
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/cost_rate_factory.rb b/vendored-plugins/openproject-costs/spec/factories/cost_rate_factory.rb
new file mode 100644
index 0000000000..f3d706ce3c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/cost_rate_factory.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :cost_rate do
+ association :cost_type, factory: :cost_type
+ valid_from Date.today
+ rate 50.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/cost_type_factory.rb b/vendored-plugins/openproject-costs/spec/factories/cost_type_factory.rb
new file mode 100644
index 0000000000..78d9636a8c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/cost_type_factory.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :cost_type do
+ sequence(:name) do |n| "ct no. #{n}" end
+ unit 'singular_unit'
+ unit_plural 'plural_unit'
+
+ trait :deleted do
+ deleted_at Time.now
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/default_hourly_rate_factory.rb b/vendored-plugins/openproject-costs/spec/factories/default_hourly_rate_factory.rb
new file mode 100644
index 0000000000..943b830f71
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/default_hourly_rate_factory.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :default_hourly_rate do
+ association :user, factory: :user
+ valid_from Date.today
+ rate 50.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/hourly_rate_factory.rb b/vendored-plugins/openproject-costs/spec/factories/hourly_rate_factory.rb
new file mode 100644
index 0000000000..d62c9a5ade
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/hourly_rate_factory.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :hourly_rate do
+ association :user, factory: :user
+ association :project, factory: :project
+ valid_from Date.today
+ rate 50.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/labor_budget_item_factory.rb b/vendored-plugins/openproject-costs/spec/factories/labor_budget_item_factory.rb
new file mode 100644
index 0000000000..95b7f7a2cc
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/labor_budget_item_factory.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :labor_budget_item do
+ association :user, factory: :user
+ association :cost_object, factory: :variable_cost_object
+ hours 0.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/material_budget_item_factory.rb b/vendored-plugins/openproject-costs/spec/factories/material_budget_item_factory.rb
new file mode 100644
index 0000000000..337988df1a
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/material_budget_item_factory.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :material_budget_item do
+ association :cost_type, factory: :cost_type
+ association :cost_object, factory: :variable_cost_object
+ units 0.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/rate_factory.rb b/vendored-plugins/openproject-costs/spec/factories/rate_factory.rb
new file mode 100644
index 0000000000..fb2e0cac32
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/rate_factory.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :rate do
+ valid_from Date.today
+ rate 50.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/factories/variable_cost_object_factory.rb b/vendored-plugins/openproject-costs/spec/factories/variable_cost_object_factory.rb
new file mode 100644
index 0000000000..2acb41fb67
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/factories/variable_cost_object_factory.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :variable_cost_object do
+ association :project, factory: :project
+ sequence(:subject) do |n| "Cost Object No. #{n}" end
+ sequence(:description) do |n| "I am a Cost Object No. #{n}" end
+ association :author, factory: :user
+ fixed_date Time.now
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/features/add_buget_spec.rb b/vendored-plugins/openproject-costs/spec/features/add_buget_spec.rb
new file mode 100644
index 0000000000..61458b98a2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/features/add_buget_spec.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe 'adding a new budget', type: :feature, js: true do
+ let(:project) { FactoryGirl.create :project_with_types }
+ let(:user) { FactoryGirl.create :admin }
+
+ before do
+ allow(User).to receive(:current).and_return user
+ end
+
+ it 'shows link to create a new budget' do
+ visit projects_cost_objects_path(project)
+
+ click_on("Add budget")
+
+ expect(page).to have_content "New budget"
+ expect(page).to have_content "Description"
+ expect(page).to have_content "Subject"
+ end
+
+ it 'create the budget' do
+ visit new_projects_cost_object_path(project)
+
+ fill_in("Subject", with: 'My subject')
+
+ click_on "Create"
+
+ expect(page).to have_content "Successful creation"
+ expect(page).to have_content "My subject"
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/features/members_hourly_rates_spec.rb b/vendored-plugins/openproject-costs/spec/features/members_hourly_rates_spec.rb
new file mode 100644
index 0000000000..f7556426b2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/features/members_hourly_rates_spec.rb
@@ -0,0 +1,94 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe 'hourly rates on a member', type: :feature, js: true do
+ let(:project) { FactoryGirl.build :project }
+ let(:user) { FactoryGirl.create :admin, member_in_project: project,
+ member_through_role: [FactoryGirl.create(:role)] }
+ let(:member) { Member.find_by(project: project, user: user) }
+
+ def view_rates
+ visit edit_user_path(user, tab: 'rates')
+ end
+
+ def view_project_members
+ visit project_members_path(project)
+ end
+
+ def expect_current_rate_in_members_table(amount)
+ view_project_members
+
+ expect(page).to have_selector("#member-#{member.id} .currency", text: amount)
+ end
+
+ def add_rate(date: nil, rate:)
+ if page.has_no_selector?("tr[id^='user_new_rate_attributes_']", wait: 1)
+ click_link_or_button 'Add rate'
+ end
+
+ within "tr[id^='user_new_rate_attributes_']" do
+ fill_in 'Valid from', with: date.strftime('%Y-%m-%d') if date
+ fill_in 'Rate', with: rate
+ end
+ end
+
+ def change_rate_date(from:, to:)
+ input = find("table.rates .date[value='#{from.strftime('%Y-%m-%d')}']")
+ input.set(to.strftime('%Y-%m-%d'))
+ end
+
+ before do
+ project.save!
+ allow(User).to receive(:current).and_return user
+ end
+
+ it 'displays always the currently active rate' do
+ if ENV['CI']
+ pending 'this spec is failing on travis but is green locally'
+ end
+
+ expect_current_rate_in_members_table('0.00 EUR')
+
+ click_link('0.00 EUR')
+
+ add_rate(rate: 10)
+
+ click_button 'Save'
+
+ expect_current_rate_in_members_table('10.00 EUR')
+
+ click_link('10.00 EUR')
+
+ add_rate(date: 3.days.ago, rate: 20)
+
+ click_button 'Save'
+
+ expect_current_rate_in_members_table('10.00 EUR')
+
+ click_link('10.00 EUR')
+
+ change_rate_date(from: Date.today, to: 5.days.ago)
+
+ click_button 'Save'
+
+ expect_current_rate_in_members_table('20.00 EUR')
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/features/users_hourly_rates_spec.rb b/vendored-plugins/openproject-costs/spec/features/users_hourly_rates_spec.rb
new file mode 100644
index 0000000000..155d16417f
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/features/users_hourly_rates_spec.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe 'hourly rates on user edit', type: :feature, js: true do
+ let(:user) { FactoryGirl.create :admin }
+
+ def view_rates
+ visit edit_user_path(user, tab: 'rates')
+ end
+
+ before do
+ allow(User).to receive(:current).and_return user
+ end
+
+ context 'with no rates' do
+ before do
+ view_rates
+ end
+
+ it 'shows no data message' do
+ expect(page).to have_text 'No data to display'
+ end
+ end
+
+ context 'with rates' do
+ let!(:rate) { FactoryGirl.create(:default_hourly_rate, user: user) }
+
+ before do
+ view_rates
+ end
+
+ it 'shows the rates' do
+ expect(page).to have_text 'Current rate'.upcase
+ end
+
+ describe 'deleting all rates' do
+ before do
+ click_link 'Update' # go to update view for rates
+ click_on 'Delete' # delete last existing rate
+ click_on 'Save' # save change
+ end
+
+ # regression test: clicking save used to result in a error
+ it 'leads back to the now empty rate overview' do
+ expect(page).to have_text /rate history/i
+ expect(page).to have_text 'No data to display'
+
+ expect(page).not_to have_text 'Current rate'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/helpers/cost_objects_helper_spec.rb b/vendored-plugins/openproject-costs/spec/helpers/cost_objects_helper_spec.rb
new file mode 100644
index 0000000000..81e907fd89
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/helpers/cost_objects_helper_spec.rb
@@ -0,0 +1,63 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe CostObjectsHelper, type: :helper do
+ let(:project) { FactoryGirl.build(:project) }
+ let(:cost_object) { FactoryGirl.build(:cost_object, project: project) }
+
+ describe '#cost_objects_to_csv' do
+ describe 'WITH a list of one cost object' do
+ it 'should output the cost objects attributes' do
+ expected = [cost_object.id,
+ cost_object.project.name,
+ cost_object.subject,
+ cost_object.author.name,
+ helper.format_date(cost_object.fixed_date),
+ helper.number_to_currency(cost_object.material_budget),
+ helper.number_to_currency(cost_object.labor_budget),
+ helper.number_to_currency(cost_object.spent),
+ helper.format_time(cost_object.created_on),
+ helper.format_time(cost_object.updated_on),
+ cost_object.description
+ ].join(I18n.t(:general_csv_separator))
+
+ expect(cost_objects_to_csv([cost_object]).include?(expected)).to be_truthy
+ end
+
+ it 'should start with a header explaining the fields' do
+ expected = ['#',
+ Project.model_name.human,
+ CostObject.human_attribute_name(:subject),
+ CostObject.human_attribute_name(:author),
+ CostObject.human_attribute_name(:fixed_date),
+ VariableCostObject.human_attribute_name(:material_budget),
+ VariableCostObject.human_attribute_name(:labor_budget),
+ CostObject.human_attribute_name(:spent),
+ CostObject.human_attribute_name(:created_on),
+ CostObject.human_attribute_name(:updated_on),
+ CostObject.human_attribute_name(:description)
+ ].join(I18n.t(:general_csv_separator))
+
+ expect(cost_objects_to_csv([cost_object]).start_with?(expected)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/budgets/budget_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/budgets/budget_representer_spec.rb
new file mode 100644
index 0000000000..627084d107
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/budgets/budget_representer_spec.rb
@@ -0,0 +1,63 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::Budgets::BudgetRepresenter do
+ let(:project) { FactoryGirl.build(:project, id: 999) }
+ let(:user) {
+ FactoryGirl.build(:user,
+ member_in_project: project,
+ created_on: 1.day.ago,
+ updated_on: Date.today)
+ }
+ let(:budget) {
+ FactoryGirl.build(:cost_object,
+ author: user,
+ project: project,
+ created_on: 1.day.ago,
+ updated_on: Date.today)
+ }
+
+ let(:representer) { described_class.new(budget, current_user: user) }
+
+ context 'generation' do
+ subject(:generated) { representer.to_json }
+
+ describe 'self link' do
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'self' }
+ let(:href) { "/api/v3/budgets/#{budget.id}" }
+ let(:title) { budget.subject }
+ end
+ end
+
+ it 'indicates its type' do
+ is_expected.to be_json_eql('Budget'.to_json).at_path('_type')
+ end
+
+ it 'indicates its id' do
+ is_expected.to be_json_eql(budget.id.to_json).at_path('id')
+ end
+
+ it 'indicates its subject' do
+ is_expected.to be_json_eql(budget.subject.to_json).at_path('subject')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb
new file mode 100644
index 0000000000..86e83632c3
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/aggregated_cost_entry_representer_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::AggregatedCostEntryRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, id: 42) }
+ let(:representer) { described_class.new(cost_entry.cost_type, cost_entry.units) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('AggregatedCostEntry'.to_json).at_path('_type')
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'costType' }
+ let(:href) { api_v3_paths.cost_type cost_entry.cost_type.id }
+ let(:title) { cost_entry.cost_type.name }
+ end
+
+ it 'has spent units' do
+ is_expected.to be_json_eql(cost_entry.units.to_json).at_path('spentUnits')
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb
new file mode 100644
index 0000000000..f8cc37eaae
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/cost_entry_representer_spec.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::CostEntryRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, id: 42) }
+ let(:representer) { described_class.new(cost_entry, current_user: double('current_user')) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('CostEntry'.to_json).at_path('_type')
+ end
+
+ it_behaves_like 'has an untitled link' do
+ let(:link) { 'self' }
+ let(:href) { api_v3_paths.cost_entry cost_entry.id }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'project' }
+ let(:href) { api_v3_paths.project cost_entry.project.id }
+ let(:title) { cost_entry.project.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'user' }
+ let(:href) { api_v3_paths.user cost_entry.user.id }
+ let(:title) { cost_entry.user.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'costType' }
+ let(:href) { api_v3_paths.cost_type cost_entry.cost_type.id }
+ let(:title) { cost_entry.cost_type.name }
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'workPackage' }
+ let(:href) { api_v3_paths.work_package cost_entry.work_package.id }
+ let(:title) { cost_entry.work_package.subject }
+ end
+
+ it 'has an id' do
+ is_expected.to be_json_eql(cost_entry.id.to_json).at_path('id')
+ end
+
+ it 'has spent units' do
+ is_expected.to be_json_eql(cost_entry.units.to_json).at_path('spentUnits')
+ end
+
+ it_behaves_like 'has ISO 8601 date only' do
+ let(:date) { cost_entry.spent_on }
+ let(:json_path) { 'spentOn' }
+ end
+
+ it_behaves_like 'has UTC ISO 8601 date and time' do
+ let(:date) { cost_entry.created_on }
+ let(:json_path) { 'createdAt' }
+ end
+
+ it_behaves_like 'has UTC ISO 8601 date and time' do
+ let(:date) { cost_entry.updated_on }
+ let(:json_path) { 'updatedAt' }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb
new file mode 100644
index 0000000000..b5480c5934
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_entries/work_package_costs_by_type_representer_spec.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostEntries::WorkPackageCostsByTypeRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:project) { FactoryGirl.create(:project) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ let(:cost_type_A) { FactoryGirl.create(:cost_type) }
+ let(:cost_type_B) { FactoryGirl.create(:cost_type) }
+ let(:cost_entries_A) {
+ FactoryGirl.create_list(:cost_entry,
+ 2,
+ units: 1,
+ work_package: work_package,
+ project: project,
+ cost_type: cost_type_A)
+ }
+ let(:cost_entries_B) {
+ FactoryGirl.create_list(:cost_entry,
+ 3,
+ units: 2,
+ work_package: work_package,
+ project: project,
+ cost_type: cost_type_B)
+ }
+ let(:current_user) {
+ FactoryGirl.build(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.build(:role, permissions: [:view_cost_entries]) }
+
+ let(:representer) { described_class.new(work_package, current_user: current_user) }
+
+ subject { representer.to_json }
+
+ before do
+ # create the lists
+ cost_entries_A
+ cost_entries_B
+ end
+
+ it 'has a type' do
+ is_expected.to be_json_eql('Collection'.to_json).at_path('_type')
+ end
+
+ it 'has one element per type' do
+ is_expected.to have_json_size(2).at_path('_embedded/elements')
+ end
+
+ it 'indicates the cost types' do
+ elements = JSON.parse(subject)['_embedded']['elements']
+ types = elements.map { |entry| entry['_links']['costType']['href'] }
+ expect(types).to include(api_v3_paths.cost_type cost_type_A.id)
+ expect(types).to include(api_v3_paths.cost_type cost_type_B.id)
+ end
+
+ it 'aggregates the units' do
+ elements = JSON.parse(subject)['_embedded']['elements']
+ units_by_type = elements.inject({}) { |hash, entry|
+ hash[entry['_links']['costType']['href']] = entry['spentUnits']
+ hash
+ }
+
+ expect(units_by_type[api_v3_paths.cost_type cost_type_A.id]).to eql 2.0
+ expect(units_by_type[api_v3_paths.cost_type cost_type_B.id]).to eql 6.0
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
new file mode 100644
index 0000000000..51515af930
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/cost_types/cost_type_representer_spec.rb
@@ -0,0 +1,59 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::CostTypes::CostTypeRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:cost_type) { FactoryGirl.build_stubbed(:cost_type) }
+ let(:representer) { described_class.new(cost_type, current_user: double('current_user')) }
+
+ subject { representer.to_json }
+
+ it 'has a type' do
+ is_expected.to be_json_eql('CostType'.to_json).at_path('_type')
+ end
+
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'self' }
+ let(:href) { api_v3_paths.cost_type cost_type.id }
+ let(:title) { cost_type.name }
+ end
+
+ it 'has an id' do
+ is_expected.to be_json_eql(cost_type.id.to_json).at_path('id')
+ end
+
+ it 'has a name' do
+ is_expected.to be_json_eql(cost_type.name.to_json).at_path('name')
+ end
+
+ it 'has a unit' do
+ is_expected.to be_json_eql(cost_type.unit.to_json).at_path('unit')
+ end
+
+ it 'has a pluralized unit' do
+ is_expected.to be_json_eql(cost_type.unit_plural.to_json).at_path('unitPlural')
+ end
+
+ it 'indicates if it is the default' do
+ is_expected.to be_json_eql(cost_type.default.to_json).at_path('isDefault')
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/path_helper_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/path_helper_spec.rb
new file mode 100644
index 0000000000..ca43215904
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/path_helper_spec.rb
@@ -0,0 +1,69 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::Utilities::PathHelper do
+ let(:helper) { Class.new.tap { |c| c.extend(described_class) }.api_v3_paths }
+
+ describe '#cost_entry' do
+ subject { helper.cost_entry 42 }
+
+ it { is_expected.to eql('/api/v3/cost_entries/42') }
+ end
+
+ describe '#cost_entries_by_work_package' do
+ subject { helper.cost_entries_by_work_package 42 }
+
+ it { is_expected.to eql('/api/v3/work_packages/42/cost_entries') }
+ end
+
+ describe '#summarized_work_package_costs_by_type' do
+ subject { helper.summarized_work_package_costs_by_type 42 }
+
+ it { is_expected.to eql('/api/v3/work_packages/42/summarized_costs_by_type') }
+ end
+
+ describe '#cost_type' do
+ subject { helper.cost_type 42 }
+
+ it { is_expected.to eql('/api/v3/cost_types/42') }
+ end
+
+ describe '#budget' do
+ subject { helper.budget 42 }
+
+ it { is_expected.to eql('/api/v3/budgets/42') }
+ end
+
+ describe '#budgets_by_project' do
+ subject { helper.budgets_by_project 42 }
+
+ it { is_expected.to eql('/api/v3/projects/42/budgets') }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/specific_work_package_schema_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/specific_work_package_schema_spec.rb
new file mode 100644
index 0000000000..b72a521256
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/specific_work_package_schema_spec.rb
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema do
+ let(:project) {
+ FactoryGirl.build(:project)
+ }
+ let(:type) { FactoryGirl.build(:type) }
+ let(:work_package) { FactoryGirl.build(:work_package, project: project, type: type) }
+
+ describe '#assignable_cost_objects' do
+ subject { described_class.new(work_package: work_package) }
+
+ before do
+ allow(project).to receive(:cost_objects).and_return(double('CostObjects'))
+ end
+
+ it 'returns project.cost_objects' do
+ expect(subject.assignable_values(:cost_object, nil)).to eql(project.cost_objects)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
new file mode 100644
index 0000000000..0f7df0cebf
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_representer_spec.rb
@@ -0,0 +1,246 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::WorkPackageRepresenter do
+ include API::V3::Utilities::PathHelper
+
+ let(:project) { FactoryGirl.create(:project) }
+ let(:role) {
+ FactoryGirl.create(:role, permissions: [:view_time_entries,
+ :view_cost_entries,
+ :view_cost_rates,
+ :view_work_packages])
+ }
+ let(:own_time_entries_role) {
+ FactoryGirl.create(:role, permissions: [:view_time_entries,
+ :view_cost_entries,
+ :view_cost_rates,
+ :view_work_packages])
+ }
+ let(:user) {
+ FactoryGirl.create(:user,
+ member_in_project: project,
+ member_through_role: role)
+ }
+
+ let(:cost_object) { FactoryGirl.create(:cost_object, project: project) }
+ let(:cost_entry_1) {
+ FactoryGirl.create(:cost_entry,
+ work_package: work_package,
+ project: project,
+ units: 3,
+ spent_on: Date.today,
+ user: user,
+ comments: 'Entry 1')
+ }
+ let(:cost_entry_2) {
+ FactoryGirl.create(:cost_entry,
+ work_package: work_package,
+ project: project,
+ units: 3,
+ spent_on: Date.today,
+ user: user,
+ comments: 'Entry 2')
+ }
+
+ let(:work_package) {
+ FactoryGirl.create(:work_package,
+ project_id: project.id,
+ cost_object: cost_object)
+ }
+ let(:representer) {
+ described_class.new(work_package,
+ current_user: user,
+ embed_links: true)
+ }
+
+ before(:each) do
+ allow(User).to receive(:current).and_return user
+ end
+
+ subject(:generated) { representer.to_json }
+
+ describe 'generation' do
+ before do
+ cost_entry_1
+ cost_entry_2
+ end
+
+ describe 'work_package' do
+ # specifiying as it used to be different
+ it { is_expected.to have_json_path('spentTime') }
+
+ it { is_expected.not_to have_json_path('spentHours') }
+
+ it { is_expected.to have_json_path('overallCosts') }
+
+ describe 'budget' do
+ it_behaves_like 'has a titled link' do
+ let(:link) { 'costObject' }
+ let(:href) { "/api/v3/budgets/#{cost_object.id}" }
+ let(:title) { cost_object.subject }
+ end
+
+ it 'has the budget embedded' do
+ is_expected.to be_json_eql(cost_object.subject.to_json)
+ .at_path('_embedded/costObject/subject')
+ end
+ end
+
+ it_behaves_like 'has an untitled link' do
+ let(:link) { 'costsByType' }
+ let(:href) { api_v3_paths.summarized_work_package_costs_by_type work_package.id }
+ end
+
+ it 'embeds the costsByType' do
+ is_expected.to have_json_path('_embedded/costsByType')
+ end
+
+ describe 'spentTime' do
+ context 'time entry with single hour' do
+ let(:time_entry) {
+ FactoryGirl.create(:time_entry,
+ project: work_package.project,
+ work_package: work_package,
+ hours: 1.0)
+ }
+
+ before do time_entry end
+
+ it { is_expected.to be_json_eql('PT1H'.to_json).at_path('spentTime') }
+ end
+
+ context 'time entry with multiple hours' do
+ let(:time_entry) {
+ FactoryGirl.create(:time_entry,
+ project: work_package.project,
+ work_package: work_package,
+ hours: 42.5)
+ }
+
+ before do time_entry end
+
+ it { is_expected.to be_json_eql('P1DT18H30M'.to_json).at_path('spentTime') }
+ end
+
+ context 'no view_time_entries permission' do
+ before do
+ allow(user).to receive(:allowed_to?).and_return false
+ end
+
+ it { is_expected.not_to have_json_path('spentTime') }
+ end
+
+ context 'only view_own_time_entries permission' do
+ let(:own_time_entries_role) {
+ FactoryGirl.create(:role, permissions: [:view_own_time_entries,
+ :view_work_packages])
+ }
+
+ let(:user2) {
+ FactoryGirl.create(:user,
+ member_in_project: project,
+ member_through_role: own_time_entries_role)
+ }
+
+ let!(:own_time_entry) {
+ FactoryGirl.create(:time_entry,
+ project: work_package.project,
+ work_package: work_package,
+ hours: 2,
+ user: user2)
+ }
+
+ let!(:other_time_entry) {
+ FactoryGirl.create(:time_entry,
+ project: work_package.project,
+ work_package: work_package,
+ hours: 1,
+ user: user)
+ }
+
+ before do
+ allow(User).to receive(:current).and_return(user2)
+ end
+
+ it { is_expected.to be_json_eql('PT2H'.to_json).at_path('spentTime') }
+ end
+
+ context 'no time entry' do
+ it { is_expected.to be_json_eql('PT0S'.to_json).at_path('spentTime') }
+ end
+ end
+ end
+ end
+
+ describe '_links' do
+ describe 'move' do
+ it_behaves_like 'action link' do
+ let(:action) { 'log_costs' }
+ let(:permission) { :log_costs }
+ end
+ end
+
+ describe 'timeEntries' do
+ it 'exists if user has view_time_entries permission' do
+ allow(user).to receive(:allowed_to?).and_return false
+ allow(user).to receive(:allowed_to?).with(:view_time_entries,
+ cost_object.project)
+ .and_return true
+
+ is_expected.to have_json_path('_links/timeEntries/href')
+ end
+
+ it 'has spentTime link when user only has view_own_time_entries permission' do
+ allow(user).to receive(:allowed_to?).and_return false
+ allow(user).to receive(:allowed_to?).with(:view_own_time_entries,
+ cost_object.project)
+ .and_return true
+
+ is_expected.to have_json_path('_links/timeEntries/href')
+ end
+ end
+ end
+
+ describe 'costs module disabled' do
+ before do
+ allow(work_package).to receive(:costs_enabled?).and_return false
+ end
+
+ describe 'work_package' do
+ it { is_expected.to have_json_path('spentTime') }
+
+ it { is_expected.not_to have_json_path('spentHours') }
+
+ it { is_expected.not_to have_json_path('overallCosts') }
+
+ describe 'embedded' do
+ it { is_expected.not_to have_json_path('_embedded/costObject') }
+
+ it { is_expected.not_to have_json_path('_embedded/summarizedCostEntries') }
+ end
+ end
+
+ describe '_links' do
+ it { is_expected.not_to have_json_path('_links/log_costs') }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
new file mode 100644
index 0000000000..f2de861355
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/api/v3/work_packages/work_package_schema_representer_spec.rb
@@ -0,0 +1,256 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ::API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
+ let(:custom_field) { FactoryGirl.build(:custom_field) }
+ let(:work_package) { FactoryGirl.build(:work_package) }
+ let(:current_user) {
+ FactoryGirl.build(:user, member_in_project: work_package.project)
+ }
+ let(:schema) {
+ ::API::V3::WorkPackages::Schema::SpecificWorkPackageSchema.new(work_package: work_package)
+ }
+ let(:embedded) { false }
+ let(:representer) {
+ described_class.create(schema,
+ form_embedded: embedded,
+ current_user: current_user)
+ }
+ subject { representer.to_json }
+
+ shared_examples_for 'has a collection of allowed values' do
+ let(:embedded) { true }
+
+ before do
+ allow(schema).to receive(:assignable_values).and_return(nil)
+ end
+
+ context 'when no values are allowed' do
+ before do
+ allow(schema).to receive(:assignable_values).with(factory, anything).and_return([])
+ end
+
+ it_behaves_like 'links to and embeds allowed values directly' do
+ let(:path) { json_path }
+ let(:hrefs) { [] }
+ end
+ end
+
+ context 'when values are allowed' do
+ let(:values) { FactoryGirl.build_stubbed_list(factory, 3) }
+
+ before do
+ allow(schema).to receive(:assignable_values).with(factory, anything).and_return(values)
+ end
+
+ it_behaves_like 'links to and embeds allowed values directly' do
+ let(:path) { json_path }
+ let(:hrefs) { values.map { |value| "/api/v3/#{href_path}/#{value.id}" } }
+ end
+ end
+
+ context 'when not embedded' do
+ before do
+ allow(schema).to receive(:assignable_values).with(factory, anything).and_return(nil)
+ end
+
+ it_behaves_like 'does not link to allowed values' do
+ let(:path) { json_path }
+ end
+ end
+ end
+
+ describe 'spentTime' do
+ shared_examples_for 'spentTime visible' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'spentTime' }
+ let(:type) { 'Duration' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.spent_time') }
+ let(:required) { true }
+ let(:writable) { false }
+ end
+ end
+
+ shared_examples_for 'spentTime not visible' do
+ it { is_expected.not_to have_json_path('spentTime') }
+ end
+
+ let(:can_view_time_entries) { false }
+ let(:can_view_own_time_entries) { false }
+
+ before do
+ allow(current_user).to receive(:allowed_to?).and_return(false)
+ allow(current_user).to receive(:allowed_to?).with(:view_time_entries, work_package.project)
+ .and_return can_view_time_entries
+ allow(current_user).to receive(:allowed_to?).with(:view_own_time_entries, work_package.project)
+ .and_return can_view_own_time_entries
+ end
+
+ context 'costs enabled' do
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return true
+ end
+
+ context 'with no time entry permissions' do
+ it_behaves_like 'spentTime not visible'
+ end
+
+ context 'with :view_time_entries permission' do
+ let(:can_view_time_entries) { true }
+ it_behaves_like 'spentTime visible'
+ end
+
+ context 'with :view_own_time_entries permission' do
+ let(:can_view_own_time_entries) { true }
+ it_behaves_like 'spentTime visible'
+ end
+ end
+
+ context 'costs disabled' do
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return false
+ end
+
+ context 'with no time entry permissions' do
+ it_behaves_like 'spentTime not visible'
+ end
+
+ context 'with :view_time_entries permission' do
+ let(:can_view_time_entries) { true }
+ it_behaves_like 'spentTime visible'
+ end
+
+ context 'with :view_own_time_entries permission' do
+ let(:can_view_own_time_entries) { true }
+ it_behaves_like 'spentTime not visible'
+ end
+ end
+ end
+
+ describe 'overallCosts' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'overallCosts' }
+ let(:type) { 'String' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.overall_costs') }
+ let(:required) { false }
+ let(:writable) { false }
+ end
+
+ context 'costs disabled' do
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return(false)
+ end
+
+ it 'has no schema for overallCosts' do
+ is_expected.not_to have_json_path('overallCosts')
+ end
+ end
+ end
+
+ describe 'costsByType' do
+ shared_examples_for 'costsByType visible' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'costsByType' }
+ let(:type) { 'Collection' }
+ let(:name) { I18n.t('activerecord.attributes.work_package.spent_units') }
+ let(:required) { false }
+ let(:writable) { false }
+ end
+ end
+
+ shared_examples_for 'costsByType not visible' do
+ it { is_expected.not_to have_json_path('costsByType') }
+ end
+
+ let(:can_view_cost_entries) { false }
+ let(:can_view_own_cost_entries) { false }
+
+ before do
+ allow(current_user).to receive(:allowed_to?).and_return(false)
+ allow(current_user).to receive(:allowed_to?).with(:view_cost_entries, work_package.project)
+ .and_return can_view_cost_entries
+ allow(current_user).to receive(:allowed_to?).with(:view_own_cost_entries, work_package.project)
+ .and_return can_view_own_cost_entries
+ end
+
+ context 'costs disabled, but all permissions' do
+ let(:can_view_cost_entries) { true }
+ let(:can_view_own_cost_entries) { true }
+
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return(false)
+ end
+
+ it_behaves_like 'costsByType not visible'
+ end
+
+ context 'costs enabled' do
+ context 'no permissions' do
+ it_behaves_like 'costsByType not visible'
+ end
+
+ context 'can only view own cost entries' do
+ let(:can_view_own_cost_entries) { true }
+ it_behaves_like 'costsByType visible'
+ end
+
+ context 'can view all cost entries' do
+ let(:can_view_cost_entries) { true }
+ it_behaves_like 'costsByType visible'
+ end
+ end
+ end
+
+ describe 'budget' do
+ it_behaves_like 'has basic schema properties' do
+ let(:path) { 'costObject' }
+ let(:type) { 'Budget' }
+ let(:name) { I18n.t('attributes.cost_object') }
+ let(:required) { false }
+ let(:writable) { true }
+ end
+
+ it_behaves_like 'has a collection of allowed values' do
+ let(:json_path) { 'costObject' }
+ let(:href_path) { 'budgets' }
+ let(:factory) { :cost_object }
+ end
+
+ context 'costs disabled' do
+ before do
+ allow(schema.project).to receive(:costs_enabled?).and_return(false)
+ end
+
+ it 'has no schema for budget' do
+ is_expected.not_to have_json_path('costObject')
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/lib/open_project/costs/default_data_spec.rb b/vendored-plugins/openproject-costs/spec/lib/open_project/costs/default_data_spec.rb
new file mode 100644
index 0000000000..e2d666b954
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/lib/open_project/costs/default_data_spec.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe OpenProject::Costs::DefaultData do
+ let(:seeder) { BasicData::RoleSeeder.new }
+ let(:member) { OpenProject::Costs::DefaultData.member_role }
+ let(:permissions) { OpenProject::Costs::DefaultData.member_permissions }
+
+ before do
+ allow(seeder).to receive(:builtin_roles).and_return([])
+
+ seeder.seed!
+ end
+
+ it 'adds permissions to the member role' do
+ expect(member.permissions).to include *permissions
+ end
+
+ it 'is not loaded again on existing data' do
+ member.permissions.clear
+ member.save!
+
+ seeder.seed!
+ member.reload
+
+ expect(member.permissions).to be_empty
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/cost_entry_spec.rb b/vendored-plugins/openproject-costs/spec/models/cost_entry_spec.rb
new file mode 100644
index 0000000000..9c7931df22
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/cost_entry_spec.rb
@@ -0,0 +1,498 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe CostEntry, type: :model do
+ include Cost::PluginSpecHelper
+
+ let(:project) { FactoryGirl.create(:project_with_types) }
+ let(:project2) { FactoryGirl.create(:project_with_types) }
+ let(:work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ type: project.types.first,
+ author: user)
+ }
+ let(:work_package2) {
+ FactoryGirl.create(:work_package, project: project2,
+ type: project2.types.first,
+ author: user)
+ }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:klass) { CostEntry }
+ let(:cost_entry) do
+ member
+ FactoryGirl.build(:cost_entry, cost_type: cost_type,
+ project: project,
+ work_package: work_package,
+ spent_on: date,
+ units: units,
+ user: user,
+ comments: 'lorem')
+ end
+
+ let(:cost_entry2) do
+ FactoryGirl.build(:cost_entry, cost_type: cost_type,
+ project: project,
+ work_package: work_package,
+ spent_on: date,
+ units: units,
+ user: user,
+ comments: 'lorem')
+ end
+
+ let(:cost_type) do
+ cost_type = FactoryGirl.create(:cost_type)
+ [first_rate, second_rate, third_rate].each do |rate|
+ rate.cost_type = cost_type
+ rate.save!
+ end
+ cost_type.reload
+ cost_type
+ end
+ let(:first_rate) {
+ FactoryGirl.build(:cost_rate, valid_from: date - 6.days,
+ rate: 10.0)
+ }
+ let(:second_rate) {
+ FactoryGirl.build(:cost_rate, valid_from: date - 4.days,
+ rate: 100.0)
+ }
+ let(:third_rate) {
+ FactoryGirl.build(:cost_rate, valid_from: date - 2.days,
+ rate: 1000.0)
+ }
+ let(:member) {
+ FactoryGirl.create(:member, project: project,
+ roles: [role],
+ principal: user)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: []) }
+ let(:units) { 5.0 }
+ let(:date) { Date.today }
+
+ describe 'class' do
+ describe '#visible' do
+ describe "WHEN having the view_cost_entries permission
+ WHEN querying for a project
+ WHEN a cost entry from another user is defined" do
+ before do
+ is_member(project, user2, [:view_cost_entries])
+
+ cost_entry.save!
+ end
+
+ it { expect(CostEntry.visible(user2, project)).to match_array([cost_entry]) }
+ end
+
+ describe "WHEN not having the view_cost_entries permission
+ WHEN querying for a project
+ WHEN a cost entry from another user is defined" do
+ before do
+ is_member(project, user2, [])
+
+ cost_entry.save!
+ end
+
+ it { expect(CostEntry.visible(user2, project)).to match_array([]) }
+ end
+
+ describe "WHEN having the view_own_cost_entries permission
+ WHEN querying for a project
+ WHEN a cost entry from another user is defined" do
+ before do
+ is_member(project, user2, [:view_own_cost_entries])
+
+ cost_entry.save!
+ end
+
+ it { expect(CostEntry.visible(user2, project)).to match_array([]) }
+ end
+
+ describe "WHEN having the view_own_cost_entries permission
+ WHEN querying for a project
+ WHEN a cost entry from the user is defined" do
+ before do
+ is_member(project, cost_entry2.user, [:view_own_cost_entries])
+
+ cost_entry2.save!
+ end
+
+ it { expect(CostEntry.visible(cost_entry2.user, project)).to match_array([cost_entry2]) }
+ end
+ end
+ end
+
+ describe 'instance' do
+ describe '#costs' do
+ let(:fourth_rate) {
+ FactoryGirl.build(:cost_rate, valid_from: date - 1.days,
+ rate: 10000.0,
+ cost_type: cost_type)
+ }
+
+ describe 'WHEN updating the number of units' do
+ before do
+ cost_entry.spent_on = first_rate.valid_from + 1.day
+ end
+
+ it 'should update costs' do
+ (0..5).each do |units|
+ cost_entry.units = units
+ cost_entry.save!
+ expect(cost_entry.costs).to eq(first_rate.rate * units)
+ end
+ end
+ end
+
+ describe 'WHEN a new rate is added at the end' do
+ before do
+ cost_entry.save!
+ fourth_rate.save!
+ cost_entry.reload
+ end
+
+ it { expect(cost_entry.costs).to eq(fourth_rate.rate * cost_entry.units) }
+ end
+
+ describe 'WHEN a new rate is added for the future' do
+ before do
+ cost_entry.save!
+ fourth_rate.valid_from = date + 1.day
+ fourth_rate.save!
+ cost_entry.reload
+ end
+
+ it { expect(cost_entry.costs).to eq(third_rate.rate * cost_entry.units) }
+ end
+
+ describe 'WHEN a new rate is added in between' do
+ before do
+ cost_entry.save!
+ fourth_rate.valid_from = date - 3.days
+ fourth_rate.save!
+ cost_entry.reload
+ end
+
+ it { expect(cost_entry.costs).to eq(third_rate.rate * cost_entry.units) }
+ end
+
+ describe 'WHEN a rate is destroyed' do
+ before do
+ cost_entry.save!
+ third_rate.destroy
+ cost_entry.reload
+ end
+
+ it { expect(cost_entry.costs).to eq(cost_entry.units * second_rate.rate) }
+ end
+
+ describe "WHEN a rate's valid from is updated" do
+ before do
+ cost_entry.save!
+ first_rate.update_attribute(:valid_from, date - 1.days)
+ cost_entry.reload
+ end
+
+ it { expect(cost_entry.costs).to eq(cost_entry.units * first_rate.rate) }
+ end
+
+ describe 'WHEN spent on is changed' do
+ before do
+ cost_type.save!
+ cost_entry.save!
+ end
+
+ it 'should take the then active rate to calculate' do
+ (5.days.ago.to_date..Date.today).each do |time|
+ cost_entry.spent_on = time
+ cost_entry.save!
+
+ rate = CostRate
+ .where(['cost_type_id = ? AND valid_from <= ?',
+ cost_entry.cost_type.id, cost_entry.spent_on])
+ .order('valid_from DESC').first.rate
+ expect(cost_entry.costs).to eq(cost_entry.units * rate)
+ end
+ end
+ end
+ end
+
+ describe '#overridden_costs' do
+ describe 'WHEN overridden costs are seet' do
+ let(:value) { rand(500) }
+
+ before do
+ cost_entry.overridden_costs = value
+ end
+
+ it { expect(cost_entry.overridden_costs).to eq(value) }
+ end
+ end
+
+ describe '#real_costs' do
+ describe 'WHEN overrridden cost are set' do
+ let(:value) { rand(500) }
+
+ before do
+ cost_entry.overridden_costs = value
+ end
+
+ it { expect(cost_entry.real_costs).to eq(value) }
+ end
+ end
+
+ describe '#valid' do
+ before do
+ cost_entry.save!
+ end
+
+ it { expect(cost_entry).to be_valid }
+
+ describe 'WHEN no cost_type is provided' do
+ before { cost_entry.cost_type = nil }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN no project is provided' do
+ before do
+ cost_entry.project = nil
+ # unfortunately the project get's set to the work_package's project if no project is provided
+ # TODO: check if that is necessary
+ cost_entry.work_package = nil
+ end
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN no work_package is provided' do
+ before { cost_entry.work_package = nil }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN the work_package is not in the project' do
+ before { cost_entry.work_package = work_package2 }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN no units are provided' do
+ before { cost_entry.units = nil }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN no spent_on is provided' do
+ before { cost_entry.spent_on = nil }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN no user is provided' do
+ before { cost_entry.user = nil }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe "WHEN the provided user is no member of the project
+ WHEN the user is unchanged" do
+ before { member.destroy }
+
+ it { expect(cost_entry).to be_valid }
+ end
+
+ describe "WHEN the provided user is no member of the project
+ WHEN the user changes" do
+ before do
+ cost_entry.user = user2
+ member.destroy
+ end
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+
+ describe 'WHEN the cost_type is deleted' do
+ before { cost_type.deleted_at = Date.new }
+
+ it { expect(cost_entry).not_to be_valid }
+ end
+ end
+
+ describe '#user' do
+ describe 'WHEN a non existing user is provided (i.e. the user has been deleted)' do
+ before do
+ cost_entry.save!
+ user.destroy
+ end
+
+ it { expect(cost_entry.reload.user).to eq(DeletedUser.first) }
+ end
+
+ describe 'WHEN an existing user is provided' do
+ it { expect(cost_entry.user).to eq(user) }
+ end
+ end
+
+ describe '#editable_by?' do
+ describe "WHEN the user has the edit_cost_entries permission
+ WHEN the cost entry is not created by the user" do
+ before do
+ is_member(project, user2, [:edit_cost_entries])
+
+ cost_entry
+ end
+
+ it { expect(cost_entry.editable_by?(user2)).to be_truthy }
+ end
+
+ describe "WHEN the user has the edit_cost_entries permission
+ WHEN the cost entry is created by the user" do
+ before do
+ is_member(project, cost_entry2.user, [:edit_cost_entries])
+ end
+
+ it { expect(cost_entry2.editable_by?(cost_entry2.user)).to be_truthy }
+ end
+
+ describe "WHEN the user has the edit_own_cost_entries permission
+ WHEN the cost entry is created by the user" do
+ before do
+ is_member(project, cost_entry2.user, [:edit_own_cost_entries])
+
+ cost_entry2
+ end
+
+ it { expect(cost_entry2.editable_by?(cost_entry2.user)).to be_truthy }
+ end
+
+ describe "WHEN the user has the edit_own_cost_entries permission
+ WHEN the cost entry is created by another user" do
+ before do
+ is_member(project, user2, [:edit_own_cost_entries])
+
+ cost_entry
+ end
+
+ it { expect(cost_entry.editable_by?(user2)).to be_falsey }
+ end
+
+ describe "WHEN the user has no cost permission
+ WHEN the cost entry is created by the user" do
+ before do
+ is_member(project, cost_entry2.user, [])
+
+ cost_entry2
+ end
+
+ it { expect(cost_entry2.editable_by?(cost_entry2.user)).to be_falsey }
+ end
+ end
+
+ describe '#creatable_by?' do
+ describe "WHEN the user has the log costs permission
+ WHEN the cost entry is not associated to the user" do
+ before do
+ is_member(project, user2, [:log_costs])
+ end
+
+ it { expect(cost_entry.creatable_by?(user2)).to be_truthy }
+ end
+
+ describe "WHEN the user has the log_costs permission
+ WHEN the cost entry is associated to user" do
+ before do
+ is_member(project, cost_entry2.user, [:log_costs])
+ end
+
+ it { expect(cost_entry2.creatable_by?(cost_entry2.user)).to be_truthy }
+ end
+
+ describe "WHEN the user has the log own costs permission
+ WHEN the cost entry is associated to the user" do
+ before do
+ is_member(project, cost_entry2.user, [:log_own_costs])
+ end
+
+ it { expect(cost_entry2.creatable_by?(cost_entry2.user)).to be_truthy }
+ end
+
+ describe "WHEN the user has the log_own_costs permission
+ WHEN the cost entry is created by another user" do
+ before do
+ is_member(project, user2, [:log_own_costs])
+ end
+
+ it { expect(cost_entry.creatable_by?(user2)).to be_falsey }
+ end
+
+ describe "WHEN the user has no cost permission
+ WHEN the cost entry is associated to the user" do
+ before do
+ is_member(project, cost_entry2.user, [])
+ end
+
+ it { expect(cost_entry2.creatable_by?(cost_entry2.user)).to be_falsey }
+ end
+ end
+
+ describe '#costs_visible_by?' do
+ describe "WHEN the user has the view_cost_rates permission
+ WHEN the cost entry is not associated to the user" do
+ before do
+ is_member(project, user2, [:view_cost_rates])
+ end
+
+ it { expect(cost_entry.costs_visible_by?(user2)).to be_truthy }
+ end
+
+ describe "WHEN the user has the view_cost_rates permission in another project
+ WHEN the cost entry is not associated to the user" do
+ before do
+ is_member(project2, user2, [:view_cost_rates])
+ end
+
+ it { expect(cost_entry.costs_visible_by?(user2)).to be_falsey }
+ end
+
+ describe "WHEN the user lacks the view_cost_rates permission
+ WHEN the cost entry is associated to the user
+ WHEN the costs are overridden" do
+ before do
+ is_member(project, cost_entry2.user, [])
+ cost_entry2.update_attribute(:overridden_costs, 1.0)
+ end
+
+ it { expect(cost_entry2.costs_visible_by?(cost_entry2.user)).to be_truthy }
+ end
+
+ describe "WHEN the user lacks the view_cost_rates permission
+ WHEN the cost entry is associated to the user
+ WHEN the costs are not overridden" do
+ before do
+ is_member(project, cost_entry2.user, [])
+ end
+
+ it { expect(cost_entry2.costs_visible_by?(cost_entry2.user)).to be_falsey }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/cost_type_spec.rb b/vendored-plugins/openproject-costs/spec/models/cost_type_spec.rb
new file mode 100644
index 0000000000..5f21746054
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/cost_type_spec.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
+
+describe CostType, type: :model do
+ let(:klass) { CostType }
+ let(:cost_type) {
+ klass.new name: 'ct1',
+ unit: 'singular',
+ unit_plural: 'plural'
+ }
+ before do
+ # as the spec_helper loads fixtures and they are probably needed by other tests
+ # we delete them here so they do not interfere.
+ # on the long run, fixtures should be removed
+
+ CostType.destroy_all
+ end
+
+ describe 'class' do
+ describe 'active' do
+ describe 'WHEN a CostType instance is deleted' do
+ before do
+ cost_type.deleted_at = Time.now
+ cost_type.save!
+ end
+
+ it { expect(klass.active.size).to eq(0) }
+ end
+
+ describe 'WHEN a CostType instance is not deleted' do
+ before do
+ cost_type.save!
+ end
+
+ it { expect(klass.active.size).to eq(1) }
+ it { expect(klass.active[0]).to eq(cost_type) }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/default_hourly_rate_spec.rb b/vendored-plugins/openproject-costs/spec/models/default_hourly_rate_spec.rb
new file mode 100644
index 0000000000..09b8f6475c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/default_hourly_rate_spec.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe DefaultHourlyRate, type: :model do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:rate) {
+ FactoryGirl.build(:default_hourly_rate, project: project,
+ user: user)
+ }
+
+ describe '#user' do
+ describe 'WHEN an existing user is provided' do
+ before do
+ rate.user = user
+ rate.save!
+ end
+
+ it { expect(rate.user).to eq(user) }
+ end
+
+ describe 'WHEN a non existing user is provided (i.e. the user is deleted)' do
+ before do
+ rate.user = user
+ rate.save!
+ user.destroy
+ rate.reload
+ end
+
+ it { expect(rate.user).to eq(DeletedUser.first) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/hourly_rate_spec.rb b/vendored-plugins/openproject-costs/spec/models/hourly_rate_spec.rb
new file mode 100644
index 0000000000..7c48d76aab
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/hourly_rate_spec.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe HourlyRate, type: :model do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:rate) {
+ FactoryGirl.build(:hourly_rate, project: project,
+ user: user)
+ }
+
+ describe '#user' do
+ describe 'WHEN an existing user is provided' do
+ before do
+ rate.user = user
+ rate.save!
+ end
+
+ it { expect(rate.user).to eq(user) }
+ end
+
+ describe 'WHEN a non existing user is provided (i.e. the user is deleted)' do
+ before do
+ rate.user = user
+ rate.save!
+ user.destroy
+ rate.reload
+ end
+
+ it { expect(rate.user).to eq(DeletedUser.first) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/labor_budget_item_spec.rb b/vendored-plugins/openproject-costs/spec/models/labor_budget_item_spec.rb
new file mode 100644
index 0000000000..da6b83b5ff
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/labor_budget_item_spec.rb
@@ -0,0 +1,211 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe LaborBudgetItem, type: :model do
+ include Cost::PluginSpecHelper
+ let(:item) { FactoryGirl.build(:labor_budget_item, cost_object: cost_object) }
+ let(:cost_object) { FactoryGirl.build(:variable_cost_object, project: project) }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:rate) {
+ FactoryGirl.create(:hourly_rate, user: user,
+ valid_from: Date.today - 4.days,
+ rate: 400.0,
+ project: project)
+ }
+ let(:project) { FactoryGirl.create(:valid_project) }
+ let(:project2) { FactoryGirl.create(:valid_project) }
+
+ describe '#calculated_costs' do
+ let(:default_costs) { '0.0'.to_f }
+
+ describe 'WHEN no user is associated' do
+ before do
+ item.user = nil
+ end
+
+ it { expect(item.calculated_costs).to eq(default_costs) }
+ end
+
+ describe 'WHEN no hours are defined' do
+ before do
+ item.hours = nil
+ end
+
+ it { expect(item.calculated_costs).to eq(default_costs) }
+ end
+
+ describe 'WHEN user, hours and rate are defined' do
+ before do
+ project.save!
+ item.hours = 5.0
+ item.user = user
+ rate.rate = 400.0
+ rate.save!
+ end
+
+ it { expect(item.calculated_costs).to eq(rate.rate * item.hours) }
+ end
+
+ describe "WHEN user, hours and rate are defined
+ WHEN the user is deleted" do
+ before do
+ project.save!
+ item.hours = 5.0
+ item.user = user
+ rate.rate = 400.0
+ rate.save!
+
+ user.destroy
+ end
+
+ it { expect(item.calculated_costs).to eq(rate.rate * item.hours) }
+ end
+ end
+
+ describe '#user' do
+ describe 'WHEN an existing user is provided' do
+ before do
+ item.save!
+ item.reload
+ item.update_attribute(:user_id, user.id)
+ item.reload
+ end
+
+ it { expect(item.user).to eq(user) }
+ end
+
+ describe 'WHEN a non existing user is provided (i.e. the user has been deleted)' do
+ before do
+ item.save!
+ item.reload
+ item.update_attribute(:user_id, user.id)
+ user.destroy
+ item.reload
+ end
+
+ it { expect(item.user).to eq(DeletedUser.first) }
+ it { expect(item.user_id).to eq(user.id) }
+ end
+ end
+
+ describe '#valid?' do
+ describe 'WHEN hours, cost_object and user are provided' do
+ it 'should be valid' do
+ expect(item).to be_valid
+ end
+ end
+
+ describe 'WHEN no hours are provided' do
+ before do
+ item.hours = nil
+ end
+
+ it 'should not be valid' do
+ expect(item).not_to be_valid
+ expect(item.errors[:hours]).to eq([I18n.t('activerecord.errors.messages.not_a_number')])
+ end
+ end
+
+ describe 'WHEN hours are provided as nontransformable string' do
+ before do
+ item.hours = 'test'
+ end
+
+ it 'should not be valid' do
+ expect(item).not_to be_valid
+ expect(item.errors[:hours]).to eq([I18n.t('activerecord.errors.messages.not_a_number')])
+ end
+ end
+
+ describe 'WHEN no cost_object is provided' do
+ before do
+ item.cost_object = nil
+ end
+
+ it 'should not be valid' do
+ expect(item).not_to be_valid
+ expect(item.errors[:cost_object]).to eq([I18n.t('activerecord.errors.messages.blank')])
+ end
+ end
+
+ describe 'WHEN no user is provided' do
+ before do
+ item.user = nil
+ end
+
+ it 'should not be valid' do
+ expect(item).not_to be_valid
+ expect(item.errors[:user]).to eq([I18n.t('activerecord.errors.messages.blank')])
+ end
+ end
+ end
+
+ describe '#costs_visible_by?' do
+ before do
+ project.enabled_module_names = project.enabled_module_names << 'costs_module'
+ end
+
+ describe "WHEN the item is assigned to the user
+ WHEN the user has the view_own_hourly_rate permission" do
+ before do
+ is_member(project, user, [:view_own_hourly_rate])
+
+ item.user = user
+ end
+
+ it { expect(item.costs_visible_by?(user)).to be_truthy }
+ end
+
+ describe "WHEN the item is assigned to the user
+ WHEN the user lacks permissions" do
+ before do
+ is_member(project, user, [])
+
+ item.user = user
+ end
+
+ it { expect(item.costs_visible_by?(user)).to be_falsey }
+ end
+
+ describe "WHEN the item is assigned to another user
+ WHEN the user has the view_hourly_rates permission" do
+ before do
+ is_member(project, user2, [:view_hourly_rates])
+
+ item.user = user
+ end
+
+ it { expect(item.costs_visible_by?(user2)).to be_truthy }
+ end
+
+ describe "WHEN the item is assigned to another user
+ WHEN the user has the view_hourly_rates permission in another project" do
+ before do
+ is_member(project2, user2, [:view_hourly_rates])
+
+ item.user = user
+ end
+
+ it { expect(item.costs_visible_by?(user2)).to be_falsey }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/permitted_params_spec.rb b/vendored-plugins/openproject-costs/spec/models/permitted_params_spec.rb
new file mode 100644
index 0000000000..205cd6de89
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/permitted_params_spec.rb
@@ -0,0 +1,212 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+describe PermittedParams, type: :model do
+ let(:user) { FactoryGirl.build(:user) }
+
+ describe '#cost_entry' do
+ it 'should return comments' do
+ params = ActionController::Parameters.new(cost_entry: { 'comments' => 'blubs' })
+
+ expect(PermittedParams.new(params, user).cost_entry).to eq({ 'comments' => 'blubs' })
+ end
+
+ it 'should return units' do
+ params = ActionController::Parameters.new(cost_entry: { 'units' => '5.0' })
+
+ expect(PermittedParams.new(params, user).cost_entry).to eq({ 'units' => '5.0' })
+ end
+
+ it 'should return overridden_costs' do
+ params = ActionController::Parameters.new(cost_entry: { 'overridden_costs' => '5.0' })
+
+ expect(PermittedParams.new(params, user).cost_entry).to eq({ 'overridden_costs' => '5.0' })
+ end
+
+ it 'should return spent_on' do
+ params = ActionController::Parameters.new(cost_entry: { 'spent_on' => Date.today.to_s })
+
+ expect(PermittedParams.new(params, user).cost_entry).to eq({ 'spent_on' => Date.today.to_s })
+ end
+
+ it 'should not return project_id' do
+ params = ActionController::Parameters.new(cost_entry: { 'project_id' => 42 })
+
+ expect(PermittedParams.new(params, user).cost_entry).to eq({})
+ end
+ end
+
+ describe '#cost_object' do
+ it 'should return comments' do
+ params = ActionController::Parameters.new(cost_object: { 'subject' => 'subject_test' })
+
+ expect(PermittedParams.new(params, user).cost_object).to eq({ 'subject' => 'subject_test' })
+ end
+
+ it 'should return description' do
+ params = ActionController::Parameters.new(cost_object: { 'description' => 'description_test' })
+
+ expect(PermittedParams.new(params, user).cost_object).to eq({ 'description' => 'description_test' })
+ end
+
+ it 'should return fixed_date' do
+ params = ActionController::Parameters.new(cost_object: { 'fixed_date' => '2013-05-06' })
+
+ expect(PermittedParams.new(params, user).cost_object).to eq({ 'fixed_date' => '2013-05-06' })
+ end
+
+ it 'should not return project_id' do
+ params = ActionController::Parameters.new(cost_object: { 'project_id' => 42 })
+
+ expect(PermittedParams.new(params, user).cost_object).to eq({})
+ end
+
+ context 'with budget item params' do
+ let(:params) { ActionController::Parameters.new(cost_object: budget_item_params) }
+ subject { PermittedParams.new(params, user).cost_object }
+
+ context 'of an existing material budget item' do
+ let(:budget_item_params) do
+ { 'existing_material_budget_item_attributes' => { '1' => {
+ 'units' => '100.0',
+ 'cost_type_id' => '1',
+ 'comments' => 'First package',
+ 'budget' => '5,000.00'
+ }
+ } }
+ end
+
+ it { is_expected.to eq(budget_item_params) }
+ end
+
+ context 'of a new material budget item' do
+ let(:budget_item_params) do
+ { 'new_material_budget_item_attributes' => { '1' => {
+ 'units' => '20',
+ 'cost_type_id' => '2',
+ 'comments' => 'Macbooks',
+ 'budget' => '52,000.00'
+ } } }
+ end
+
+ it { is_expected.to eq(budget_item_params) }
+ end
+
+ context 'of an existing labor budget item' do
+ let(:budget_item_params) do
+ { 'existing_labor_budget_item_attributes' => { '1' => {
+ 'hours' => '20.0',
+ 'user_id' => '1',
+ 'comments' => 'App Setup',
+ 'budget' => '2000.00'
+ } } }
+ end
+
+ it { is_expected.to eq(budget_item_params) }
+ end
+
+ context 'of a new labor budget item' do
+ let(:budget_item_params) do
+ { 'new_labor_budget_item_attributes' => { '1' => {
+ 'hours' => '5.0',
+ 'user_id' => '2',
+ 'comments' => 'Overhead',
+ 'budget' => '400'
+ } } }
+ end
+
+ it { is_expected.to eq(budget_item_params) }
+ end
+ end
+ end
+
+ describe '#cost_type' do
+ it 'should return name' do
+ params = ActionController::Parameters.new(cost_type: { 'name' => 'name_test' })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'name' => 'name_test' })
+ end
+
+ it 'should return unit' do
+ params = ActionController::Parameters.new(cost_type: { 'unit' => 'unit_test' })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'unit' => 'unit_test' })
+ end
+
+ it 'should return unit_plural' do
+ params = ActionController::Parameters.new(cost_type: { 'unit_plural' => 'unit_plural_test' })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'unit_plural' => 'unit_plural_test' })
+ end
+
+ it 'should return default' do
+ params = ActionController::Parameters.new(cost_type: { 'default' => 7 })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'default' => 7 })
+ end
+
+ it 'should return new_rate_attributes' do
+ params = ActionController::Parameters.new(cost_type: { 'new_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' }, '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'new_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' }, '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+ end
+
+ it 'should return existing_rate_attributes' do
+ params = ActionController::Parameters.new(cost_type: { 'existing_rate_attributes' => { '9' => { 'valid_from' => '2013-05-05', 'rate' => '50.0' } } })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({ 'existing_rate_attributes' => { '9' => { 'valid_from' => '2013-05-05', 'rate' => '50.0' } } })
+ end
+
+ it 'should not return project_id' do
+ params = ActionController::Parameters.new(cost_type: { 'project_id' => 42 })
+
+ expect(PermittedParams.new(params, user).cost_type).to eq({})
+ end
+ end
+
+ describe '#user_rates' do
+ it 'should return new_rate_attributes' do
+ params = ActionController::Parameters.new(user: { 'new_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' },
+ '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+
+ expect(PermittedParams.new(params, user).user_rates).to eq({ 'new_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' },
+ '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+ end
+
+ it 'should return existing_rate_attributes' do
+ params = ActionController::Parameters.new(user: { 'existing_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' },
+ '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+
+ expect(PermittedParams.new(params, user).user_rates).to eq({ 'existing_rate_attributes' => { '0' => { 'valid_from' => '2013-05-08', 'rate' => '5002' },
+ '1' => { 'valid_from' => '2013-05-10', 'rate' => '5004' } } })
+ end
+ end
+
+ describe '#new_work_package' do
+ it 'should permit cost_object_id' do
+ hash = { 'cost_object_id' => '1' }
+
+ params = ActionController::Parameters.new(work_package: hash)
+
+ expect(PermittedParams.new(params, user).new_work_package).to eq(hash)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/rate_spec.rb b/vendored-plugins/openproject-costs/spec/models/rate_spec.rb
new file mode 100644
index 0000000000..48479684d2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/rate_spec.rb
@@ -0,0 +1,94 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Rate, type: :model do
+ let(:rate) { FactoryGirl.build(:rate) }
+
+ describe '#valid?' do
+ describe 'WHEN no rate is supplied' do
+ before do
+ rate.rate = nil
+ end
+
+ it 'should not be valid' do
+ expect(rate).not_to be_valid
+ expect(rate.errors[:rate]).to eq([I18n.t('activerecord.errors.messages.not_a_number')])
+ end
+ end
+
+ describe 'WHEN no number is supplied' do
+ before do
+ rate.rate = 'test'
+ end
+
+ it 'should not be valid' do
+ expect(rate).not_to be_valid
+ expect(rate.errors[:rate]).to eq([I18n.t('activerecord.errors.messages.not_a_number')])
+ end
+ end
+
+ describe 'WHEN a rate is supplied' do
+ before do
+ rate.rate = 5.0
+ end
+
+ it { expect(rate).to be_valid }
+ end
+
+ describe 'WHEN a date is supplied' do
+ before do
+ rate.valid_from = Date.today
+ end
+
+ it { expect(rate).to be_valid }
+ end
+
+ describe 'WHEN a transformable string is supplied for date' do
+ before do
+ rate.valid_from = '2012-03-04'
+ end
+
+ it { expect(rate).to be_valid }
+ end
+
+ describe 'WHEN a nontransformable string is supplied for date' do
+ before do
+ rate.valid_from = '2012-02-30'
+ end
+
+ it 'should not be valid' do
+ expect(rate).not_to be_valid
+ expect(rate.errors[:valid_from]).to eq([I18n.t('activerecord.errors.messages.not_a_date')])
+ end
+ end
+
+ describe 'WHEN no value is supplied for date' do
+ before do
+ rate.valid_from = nil
+ end
+
+ it 'should not be valid' do
+ expect(rate).not_to be_valid
+ expect(rate.errors[:valid_from]).to eq([I18n.t('activerecord.errors.messages.not_a_date')])
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/time_entry_spec.rb b/vendored-plugins/openproject-costs/spec/models/time_entry_spec.rb
new file mode 100644
index 0000000000..2532d14323
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/time_entry_spec.rb
@@ -0,0 +1,421 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe TimeEntry, type: :model do
+ include Cost::PluginSpecHelper
+ let(:project) { FactoryGirl.create(:project_with_types, is_public: false) }
+ let(:project2) { FactoryGirl.create(:project_with_types, is_public: false) }
+ let(:work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ type: project.types.first,
+ author: user)
+ }
+ let(:work_package2) {
+ FactoryGirl.create(:work_package, project: project2,
+ type: project2.types.first,
+ author: user2)
+ }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:date) { Date.today }
+ let(:rate) { FactoryGirl.build(:cost_rate) }
+ let!(:hourly_one) { FactoryGirl.create(:hourly_rate, valid_from: 2.days.ago, project: project, user: user) }
+ let!(:hourly_three) { FactoryGirl.create(:hourly_rate, valid_from: 4.days.ago, project: project, user: user) }
+ let!(:hourly_five) { FactoryGirl.create(:hourly_rate, valid_from: 6.days.ago, project: project, user: user) }
+ let!(:default_hourly_one) { FactoryGirl.create(:default_hourly_rate, valid_from: 2.days.ago, project: project, user: user2) }
+ let!(:default_hourly_three) { FactoryGirl.create(:default_hourly_rate, valid_from: 4.days.ago, project: project, user: user2) }
+ let!(:default_hourly_five) { FactoryGirl.create(:default_hourly_rate, valid_from: 6.days.ago, project: project, user: user2) }
+ let(:hours) { 5.0 }
+ let(:time_entry) do
+ FactoryGirl.create(:time_entry, project: project,
+ work_package: work_package,
+ spent_on: date,
+ hours: hours,
+ user: user,
+ rate: hourly_one,
+ comments: 'lorem')
+ end
+
+ let(:time_entry2) do
+ FactoryGirl.create(:time_entry, project: project,
+ work_package: work_package,
+ spent_on: date,
+ hours: hours,
+ user: user,
+ rate: hourly_one,
+ comments: 'lorem')
+ end
+
+ it 'should always prefer overridden_costs' do
+ allow(User).to receive(:current).and_return(user)
+
+ value = rand(500)
+ time_entry.overridden_costs = value
+ expect(time_entry.overridden_costs).to eq(value)
+ expect(time_entry.real_costs).to eq(value)
+ time_entry.save!
+ end
+
+ describe 'given rate' do
+ before(:each) do
+ allow(User).to receive(:current).and_return(user)
+ @default_example = time_entry2
+ end
+
+ it 'should return the current costs depending on the number of hours' do
+ (0..100).each do |hours|
+ time_entry.hours = hours
+ time_entry.save!
+ expect(time_entry.costs).to eq(time_entry.rate.rate * hours)
+ end
+ end
+
+ it 'should update cost if a new rate is added at the end' do
+ time_entry.user = User.current
+ time_entry.spent_on = Time.now
+ time_entry.hours = 1
+ time_entry.save!
+ expect(time_entry.costs).to eq(hourly_one.rate)
+ (hourly = HourlyRate.new.tap do |hr|
+ hr.valid_from = 1.day.ago
+ hr.rate = 1.0
+ hr.user = User.current
+ hr.project = hourly_one.project
+ end).save!
+ time_entry.reload
+ expect(time_entry.rate).not_to eq(hourly_one)
+ expect(time_entry.costs).to eq(hourly.rate)
+ end
+
+ it 'should update cost if a new rate is added in between' do
+ time_entry.user = User.current
+ time_entry.spent_on = 3.days.ago.to_date
+ time_entry.hours = 1
+ time_entry.save!
+ expect(time_entry.costs).to eq(hourly_three.rate)
+ (hourly = HourlyRate.new.tap do |hr|
+ hr.valid_from = 3.days.ago.to_date
+ hr.rate = 1.0
+ hr.user = User.current
+ hr.project = hourly_one.project
+ end).save!
+ time_entry.reload
+ expect(time_entry.rate).not_to eq(hourly_three)
+ expect(time_entry.costs).to eq(hourly.rate)
+ end
+
+ it 'should update cost if a spent_on changes' do
+ time_entry.hours = 1
+ (5.days.ago.to_date..Date.today).each do |time|
+ time_entry.spent_on = time.to_date
+ time_entry.save!
+ expect(time_entry.costs).to eq(time_entry.user.rate_at(time, project.id).rate)
+ end
+ end
+
+ it 'should update cost if a rate is removed' do
+ time_entry.spent_on = hourly_one.valid_from
+ time_entry.hours = 1
+ time_entry.save!
+ expect(time_entry.costs).to eq(hourly_one.rate)
+ hourly_one.destroy
+ time_entry.reload
+ expect(time_entry.costs).to eq(hourly_three.rate)
+ hourly_three.destroy
+ time_entry.reload
+ expect(time_entry.costs).to eq(hourly_five.rate)
+ end
+
+ it 'should be able to change order of rates (sorted by valid_from)' do
+ time_entry.spent_on = hourly_one.valid_from
+ time_entry.save!
+ expect(time_entry.rate).to eq(hourly_one)
+ hourly_one.valid_from = hourly_three.valid_from - 1.day
+ hourly_one.save!
+ time_entry.reload
+ expect(time_entry.rate).to eq(hourly_three)
+ end
+ end
+
+ describe 'default rate' do
+ before(:each) do
+ allow(User).to receive(:current).and_return(user)
+ @default_example = time_entry2
+ end
+
+ it 'should return the current costs depending on the number of hours' do
+ (0..100).each do |hours|
+ @default_example.hours = hours
+ @default_example.save!
+ expect(@default_example.costs).to eq(@default_example.rate.rate * hours)
+ end
+ end
+
+ it 'should update cost if a new rate is added at the end' do
+ @default_example.user = user2
+ @default_example.spent_on = Time.now.to_date
+ @default_example.hours = 1
+ @default_example.save!
+ expect(@default_example.costs).to eq(default_hourly_one.rate)
+ (hourly = DefaultHourlyRate.new.tap do |dhr|
+ dhr.valid_from = 1.day.ago.to_date
+ dhr.rate = 1.0
+ dhr.user = user2
+ end).save!
+ @default_example.reload
+ expect(@default_example.rate).not_to eq(default_hourly_one)
+ expect(@default_example.costs).to eq(hourly.rate)
+ end
+
+ it 'should update cost if a new rate is added in between' do
+ @default_example.user = user2
+ @default_example.spent_on = 3.days.ago.to_date
+ @default_example.hours = 1
+ @default_example.save!
+ expect(@default_example.costs).to eq(default_hourly_three.rate)
+ (hourly = DefaultHourlyRate.new.tap do |dhr|
+ dhr.valid_from = 3.days.ago.to_date
+ dhr.rate = 1.0
+ dhr.user = user2
+ end).save!
+ @default_example.reload
+ expect(@default_example.rate).not_to eq(default_hourly_three)
+ expect(@default_example.costs).to eq(hourly.rate)
+ end
+
+ it 'should update cost if a spent_on changes' do
+ @default_example.hours = 1
+ (5.days.ago.to_date..Date.today).each do |time|
+ @default_example.spent_on = time.to_date
+ @default_example.save!
+ expect(@default_example.costs).to eq(@default_example.user.rate_at(time, project.id).rate)
+ end
+ end
+
+ it 'should update cost if a rate is removed' do
+ @default_example.spent_on = default_hourly_one.valid_from
+ @default_example.hours = 1
+ @default_example.save!
+ expect(@default_example.costs).to eq(default_hourly_one.rate)
+ default_hourly_one.destroy
+ @default_example.reload
+ expect(@default_example.costs).to eq(default_hourly_three.rate)
+ default_hourly_three.destroy
+ @default_example.reload
+ expect(@default_example.costs).to eq(default_hourly_five.rate)
+ end
+
+ it 'shoud be able to switch between default hourly rate and hourly rate' do
+ @default_example.user = user2
+ @default_example.rate = default_hourly_one
+ @default_example.save!
+ @default_example.reload
+ expect(@default_example.rate).to eq(default_hourly_one)
+
+ (rate = HourlyRate.new.tap do |hr|
+ hr.valid_from = 10.days.ago.to_date
+ hr.rate = 1337.0
+ hr.user = @default_example.user
+ hr.project = project
+ end).save!
+
+ @default_example.reload
+ expect(@default_example.rate).to eq(rate)
+ rate.destroy
+ @default_example.reload
+ expect(@default_example.rate).to eq(default_hourly_one)
+ end
+
+ describe '#costs_visible_by?' do
+ before do
+ project.enabled_module_names = project.enabled_module_names << 'costs_module'
+ end
+
+ describe "WHEN the time_entry is assigned to the user
+ WHEN the user has the view_own_hourly_rate permission" do
+ before do
+ is_member(project, user, [:view_own_hourly_rate])
+
+ time_entry.user = user
+ end
+
+ it { expect(time_entry.costs_visible_by?(user)).to be_truthy }
+ end
+
+ describe "WHEN the time_entry is assigned to the user
+ WHEN the user lacks permissions" do
+ before do
+ is_member(project, user, [])
+
+ time_entry.user = user
+ end
+
+ it { expect(time_entry.costs_visible_by?(user)).to be_falsey }
+ end
+
+ describe "WHEN the time_entry is assigned to another user
+ WHEN the user has the view_hourly_rates permission" do
+ before do
+ is_member(project, user2, [:view_hourly_rates])
+
+ time_entry.user = user
+ end
+
+ it { expect(time_entry.costs_visible_by?(user2)).to be_truthy }
+ end
+
+ describe "WHEN the time_entry is assigned to another user
+ WHEN the user has the view_hourly_rates permission in another project" do
+ before do
+ is_member(project2, user2, [:view_hourly_rates])
+
+ time_entry.user = user
+ end
+
+ it { expect(time_entry.costs_visible_by?(user2)).to be_falsey }
+ end
+ end
+ end
+
+ describe 'class' do
+ describe '#visible' do
+ describe "WHEN having the view_time_entries permission
+ WHEN querying for a project
+ WHEN a time entry from another user is defined" do
+ before do
+ is_member(project, user2, [:view_time_entries])
+
+ time_entry.save!
+ end
+
+ it { expect(TimeEntry.visible(user2, project)).to match_array([time_entry]) }
+ end
+
+ describe "WHEN not having the view_time_entries permission
+ WHEN querying for a project
+ WHEN a time entry from another user is defined" do
+ before do
+ is_member(project, user2, [])
+
+ time_entry.save!
+ end
+
+ it { expect(TimeEntry.visible(user2, project)).to match_array([]) }
+ end
+
+ describe "WHEN having the view_own_time_entries permission
+ WHEN querying for a project
+ WHEN a time entry from another user is defined" do
+ before do
+ is_member(project, user2, [:view_own_time_entries])
+ # don't understand why memberships get loaded on the user
+ time_entry2.user.memberships(true)
+
+ time_entry.save!
+ end
+
+ it { expect(TimeEntry.visible(user2, project)).to match_array([]) }
+ end
+
+ describe "WHEN having the view_own_time_entries permission
+ WHEN querying for a project
+ WHEN a time entry from the user is defined" do
+ before do
+ is_member(project, time_entry2.user, [:view_own_time_entries])
+ # don't understand why memberships get loaded on the user
+ time_entry2.user.memberships(true)
+
+ time_entry2.save!
+ end
+
+ it { expect(TimeEntry.visible(time_entry2.user, project)).to match_array([time_entry2]) }
+ end
+ end
+
+ context 'calculate dates' do
+ let(:my_role) { FactoryGirl.create(:role, permissions: [:view_own_time_entries]) }
+ let(:user3) do
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: my_role)
+ end
+
+ let(:late_time_entry) do
+ FactoryGirl.create(:time_entry,
+ project: project,
+ work_package: work_package,
+ spent_on: 2.days.ago,
+ hours: hours,
+ user: user3,
+ rate: hourly_one,
+ comments: 'ipsum')
+ end
+ let(:early_time_entry) do
+ FactoryGirl.create(:time_entry,
+ project: project,
+ work_package: work_package,
+ spent_on: 4.days.ago,
+ hours: hours,
+ user: user3,
+ rate: hourly_one,
+ comments: 'dolor')
+ end
+ let(:other_time_entry) do
+ FactoryGirl.create(:time_entry,
+ project: project,
+ work_package: work_package,
+ spent_on: 6.days.ago,
+ hours: hours,
+ user: user2,
+ rate: hourly_one,
+ comments: 'dolor')
+ end
+ let(:another_time_entry) do
+ FactoryGirl.create(:time_entry,
+ project: project,
+ work_package: work_package,
+ spent_on: 1.days.ago,
+ hours: hours,
+ user: user2,
+ rate: hourly_one,
+ comments: 'dolor')
+ end
+
+ before do
+ early_time_entry.save!
+ late_time_entry.save!
+ other_time_entry.save!
+ another_time_entry.save!
+
+ allow(User).to receive(:current).and_return(user3)
+ end
+ describe '#earliest_date_for_project' do
+ it 'returns the earliest date' do
+ expect(TimeEntry.earliest_date_for_project(project)).to eq(early_time_entry.spent_on)
+ end
+ end
+
+ describe '#latest_date_for_project' do
+ it 'returns the latest date' do
+ expect(TimeEntry.latest_date_for_project(project)).to eq(late_time_entry.spent_on)
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/user_deletion_spec.rb b/vendored-plugins/openproject-costs/spec/models/user_deletion_spec.rb
new file mode 100644
index 0000000000..0b9ceb0d13
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/user_deletion_spec.rb
@@ -0,0 +1,199 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe User, '#destroy', type: :model do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:substitute_user) { DeletedUser.first }
+ let(:project) { FactoryGirl.create(:valid_project) }
+
+ before do
+ user
+ user2
+ end
+
+ after do
+ User.current = nil
+ end
+
+ shared_examples_for 'costs updated journalized associated object' do
+ before do
+ User.current = user2
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user2)
+ end
+ associated_instance.save!
+
+ User.current = user # in order to have the content journal created by the user
+ associated_instance.reload
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user)
+ end
+ associated_instance.save!
+
+ user.destroy
+ associated_instance.reload
+ end
+
+ it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) }
+ it 'should replace the user on all associations' do
+ associations.each do |association|
+ expect(associated_instance.send(association)).to eq(substitute_user)
+ end
+ end
+ it { expect(associated_instance.journals.first.user).to eq(user2) }
+ it 'should update first journal changed_data' do
+ associations.each do |association|
+ expect(associated_instance.journals.first.changed_data["#{association}_id".to_sym].last).to eq(user2.id)
+ end
+ end
+ it { expect(associated_instance.journals.last.user).to eq(substitute_user) }
+ it 'should update second journal changed_data' do
+ associations.each do |association|
+ expect(associated_instance.journals.last.changed_data["#{association}_id".to_sym].last).to eq(substitute_user.id)
+ end
+ end
+ end
+
+ shared_examples_for 'costs created journalized associated object' do
+ before do
+ User.current = user # in order to have the content journal created by the user
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user)
+ end
+ associated_instance.save!
+
+ User.current = user2
+ associated_instance.reload
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user2)
+ end
+ associated_instance.save!
+
+ user.destroy
+ associated_instance.reload
+ end
+
+ it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) }
+ it 'should keep the current user on all associations' do
+ associations.each do |association|
+ expect(associated_instance.send(association)).to eq(user2)
+ end
+ end
+ it { expect(associated_instance.journals.first.user).to eq(substitute_user) }
+ it 'should update the first journal' do
+ associations.each do |association|
+ expect(associated_instance.journals.first.changed_data["#{association}_id".to_sym].last).to eq(substitute_user.id)
+ end
+ end
+ it { expect(associated_instance.journals.last.user).to eq(user2) }
+ it 'should update the last journal' do
+ associations.each do |association|
+ expect(associated_instance.journals.last.changed_data["#{association}_id".to_sym].first).to eq(substitute_user.id)
+ expect(associated_instance.journals.last.changed_data["#{association}_id".to_sym].last).to eq(user2.id)
+ end
+ end
+ end
+
+ describe 'WHEN the user updated a cost object' do
+ let(:associations) { [:author] }
+ let(:associated_instance) { FactoryGirl.build(:variable_cost_object) }
+ let(:associated_class) { CostObject }
+
+ it_should_behave_like 'costs updated journalized associated object'
+ end
+
+ describe 'WHEN the user created a cost object' do
+ let(:associations) { [:author] }
+ let(:associated_instance) { FactoryGirl.build(:variable_cost_object) }
+ let(:associated_class) { CostObject }
+
+ it_should_behave_like 'costs created journalized associated object'
+ end
+
+ describe 'WHEN the user has a labor_budget_item associated' do
+ let(:item) { FactoryGirl.build(:labor_budget_item, user: user) }
+
+ before do
+ item.save!
+
+ user.destroy
+ end
+
+ it { expect(LaborBudgetItem.find_by_id(item.id)).to eq(item) }
+ it { expect(item.user_id).to eq(user.id) }
+ end
+
+ describe 'WHEN the user has a cost entry' do
+ let(:work_package) { FactoryGirl.create(:work_package) }
+ let(:entry) {
+ FactoryGirl.build(:cost_entry, user: user,
+ project: work_package.project,
+ units: 100.0,
+ spent_on: Date.today,
+ work_package: work_package,
+ comments: '')
+ }
+
+ before do
+ FactoryGirl.create(:member, project: work_package.project,
+ user: user,
+ roles: [FactoryGirl.build(:role)])
+ entry.save!
+
+ user.destroy
+
+ entry.reload
+ end
+
+ it { expect(entry.user_id).to eq(user.id) }
+ end
+
+ describe 'WHEN the user is assigned an hourly rate' do
+ let(:hourly_rate) {
+ FactoryGirl.build(:hourly_rate, user: user,
+ project: project)
+ }
+
+ before do
+ hourly_rate.save!
+ user.destroy
+ end
+
+ it { expect(HourlyRate.find_by_id(hourly_rate.id)).to eq(hourly_rate) }
+ it { expect(hourly_rate.reload.user_id).to eq(user.id) }
+ end
+
+ describe 'WHEN the user is assigned a default hourly rate' do
+ let(:default_hourly_rate) {
+ FactoryGirl.build(:default_hourly_rate, user: user,
+ project: project)
+ }
+
+ before do
+ default_hourly_rate.save!
+ user.destroy
+ end
+
+ it { expect(DefaultHourlyRate.find_by_id(default_hourly_rate.id)).to eq(default_hourly_rate) }
+ it { expect(default_hourly_rate.reload.user_id).to eq(user.id) }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/user_spec.rb b/vendored-plugins/openproject-costs/spec/models/user_spec.rb
new file mode 100644
index 0000000000..eb0ff366be
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/user_spec.rb
@@ -0,0 +1,190 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe User, type: :model do
+ include Cost::PluginSpecHelper
+ let(:klass) { User }
+ let(:user) { FactoryGirl.build(:user) }
+ let(:project) { FactoryGirl.build(:valid_project) }
+ let(:project2) { FactoryGirl.build(:valid_project) }
+ let(:project_hourly_rate) {
+ FactoryGirl.build(:hourly_rate, user: user,
+ project: project)
+ }
+ let(:default_hourly_rate) { FactoryGirl.build(:default_hourly_rate, user: user) }
+
+ describe '#allowed_to' do
+ describe 'WITH querying for a non existent permission' do
+ it { expect(user.allowed_to?(:bogus_permission, project)).to be_falsey }
+ end
+ end
+
+ describe '#allowed_to_condition_with_project_id' do
+ let(:permission) { :view_own_time_entries }
+
+ before do
+ project.save!
+ project2.save!
+ end
+
+ describe "WHEN user has the permission in one project
+ WHEN not requesting for a specific project" do
+ before do
+ is_member(project, user, [permission])
+ end
+
+ it 'should return a sql condition where the project id the user has the permission in is enforced' do
+ expect(user.allowed_to_condition_with_project_id(permission)).to eq("(projects.id in (#{project.id}))")
+ end
+ end
+
+ describe "WHEN user has the permission in two projects
+ WHEN not requesting for a specific project" do
+ before do
+ is_member(project, user, [permission])
+ is_member(project2, user, [permission])
+ end
+
+ it 'should return a sql condition where all the project ids the user has the permission in is enforced' do
+ # as order is not guaranteed and in fact does not matter
+ # we have to check for both valid options
+ valid_conditions = ["(projects.id in (#{project.id}, #{project2.id}))",
+ "(projects.id in (#{project2.id}, #{project.id}))"]
+
+ expect(valid_conditions).to include(user.allowed_to_condition_with_project_id(permission))
+ end
+ end
+
+ describe "WHEN user does not have the permission in any
+ WHEN not requesting for a specific project" do
+ before do
+ user.save!
+ end
+
+ it 'should return a neutral (for an or operation) sql condition' do
+ expect(user.allowed_to_condition_with_project_id(permission)).to eq('1=0')
+ end
+ end
+
+ describe "WHEN user has the permission in two projects
+ WHEN requesting for a specific project" do
+ before do
+ is_member(project, user, [permission])
+ is_member(project2, user, [permission])
+ end
+
+ it 'should return a sql condition where all the project ids the user has the permission in is enforced' do
+ expect(user.allowed_to_condition_with_project_id(permission, project)).to eq("(projects.id in (#{project.id}))")
+ end
+ end
+ end
+
+ describe '#set_existing_rates' do
+ before do
+ user.save
+ project.save
+ end
+
+ describe "WHEN providing a project
+ WHEN providing attributes for an existing rate in the project" do
+ let(:new_attributes) {
+ { project_hourly_rate.id.to_s => { valid_from: (Date.today + 1.day).to_s,
+ rate: (project_hourly_rate.rate + 5).to_s } }
+ }
+
+ before do
+ project_hourly_rate.save!
+ user.rates(true)
+
+ user.set_existing_rates(project, new_attributes)
+ end
+
+ it 'should update the rate' do
+ expect(user.rates.detect { |r| r.id == project_hourly_rate.id }.rate).to eq(new_attributes[project_hourly_rate.id.to_s][:rate].to_i)
+ end
+
+ it 'should update valid_from' do
+ expect(user.rates.detect { |r| r.id == project_hourly_rate.id }.valid_from).to eq(new_attributes[project_hourly_rate.id.to_s][:valid_from].to_date)
+ end
+
+ it 'should not create a rate' do
+ expect(user.rates.size).to eq(1)
+ end
+ end
+
+ describe "WHEN providing a project
+ WHEN providing attributes for an existing rate in another project" do
+ let(:new_attributes) {
+ { project_hourly_rate.id.to_s => { valid_from: (Date.today + 1.day).to_s,
+ rate: (project_hourly_rate.rate + 5).to_s } }
+ }
+
+ before do
+ project_hourly_rate.save!
+ user.rates(true)
+ @original_rate = project_hourly_rate.rate
+ @original_valid_from = project_hourly_rate.valid_from
+
+ user.set_existing_rates(project2, new_attributes)
+ end
+
+ it 'should not update the rate' do
+ expect(user.rates.detect { |r| r.id == project_hourly_rate.id }.rate).to eq(@original_rate)
+ end
+
+ it 'should not update valid_from' do
+ expect(user.rates.detect { |r| r.id == project_hourly_rate.id }.valid_from).to eq(@original_valid_from)
+ end
+
+ it 'should not create a rate' do
+ expect(user.rates.size).to eq(1)
+ end
+ end
+
+ describe "WHEN providing a project
+ WHEN not providing attributes" do
+ before do
+ project_hourly_rate.save!
+ user.rates(true)
+
+ user.set_existing_rates(project, {})
+ end
+
+ it 'should delete the hourly rate' do
+ expect(user.rates(true)).to be_empty
+ end
+ end
+
+ describe "WHEN not providing a project
+ WHEN not providing attributes" do
+ before do
+ default_hourly_rate.save!
+ user.default_rates(true)
+
+ user.set_existing_rates(nil, {})
+ end
+
+ it 'should delete the default hourly rate' do
+ expect(user.default_rates(true)).to be_empty
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/variable_cost_object_spec.rb b/vendored-plugins/openproject-costs/spec/models/variable_cost_object_spec.rb
new file mode 100644
index 0000000000..3fdefdf88d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/variable_cost_object_spec.rb
@@ -0,0 +1,67 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe VariableCostObject, type: :model do
+ let(:cost_object) { FactoryGirl.build(:variable_cost_object) }
+ let(:type) { FactoryGirl.create(:type_feature) }
+ let(:project) { FactoryGirl.create(:project_with_types) }
+ let(:user) { FactoryGirl.create(:user) }
+
+ describe 'recreate initial journal' do
+ before do
+ allow(User).to receive(:current).and_return(user)
+
+ @variable_cost_object = FactoryGirl.create(:variable_cost_object, project: project,
+ author: user)
+
+ @initial_journal = @variable_cost_object.journals.first
+ @recreated_journal = @variable_cost_object.recreate_initial_journal!
+ end
+
+ it { expect(@initial_journal).to be_identical(@recreated_journal) }
+ end
+
+ describe 'initialization' do
+ let(:cost_object) { VariableCostObject.new }
+
+ before do
+ allow(User).to receive(:current).and_return(user)
+ end
+
+ it { expect(cost_object.author).to eq(user) }
+ end
+
+ describe 'destroy' do
+ let(:work_package) { FactoryGirl.create(:work_package) }
+
+ before do
+ cost_object.author = user
+ cost_object.work_packages = [work_package]
+ cost_object.save!
+
+ cost_object.destroy
+ end
+
+ it { expect(VariableCostObject.find_by_id(cost_object.id)).to be_nil }
+ it { expect(WorkPackage.find_by_id(work_package.id)).to eq(work_package) }
+ it { expect(work_package.reload.cost_object).to be_nil }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/work_package/ask_before_destruction_spec.rb b/vendored-plugins/openproject-costs/spec/models/work_package/ask_before_destruction_spec.rb
new file mode 100644
index 0000000000..3ba8e9c470
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/work_package/ask_before_destruction_spec.rb
@@ -0,0 +1,287 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe WorkPackage, type: :model do
+ let(:work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ status: status)
+ }
+ let(:work_package2) {
+ FactoryGirl.create(:work_package, project: project2,
+ status: status)
+ }
+ let(:user) { FactoryGirl.create(:user) }
+
+ let(:type) { FactoryGirl.create(:type_standard) }
+ let(:project) { FactoryGirl.create(:project, types: [type]) }
+ let(:project2) { FactoryGirl.create(:project, types: [type]) }
+ let(:role) { FactoryGirl.create(:role) }
+ let(:role2) { FactoryGirl.create(:role) }
+ let(:member) {
+ FactoryGirl.create(:member, principal: user,
+ roles: [role])
+ }
+ let(:member2) {
+ FactoryGirl.create(:member, principal: user,
+ roles: [role2],
+ project: work_package2.project)
+ }
+ let(:status) { FactoryGirl.create(:status) }
+ let(:priority) { FactoryGirl.create(:priority) }
+ let(:cost_type) { FactoryGirl.create(:cost_type) }
+ let(:cost_entry) {
+ FactoryGirl.build(:cost_entry, work_package: work_package,
+ project: work_package.project,
+ cost_type: cost_type)
+ }
+ let(:cost_entry2) {
+ FactoryGirl.build(:cost_entry, work_package: work_package2,
+ project: work_package2.project,
+ cost_type: cost_type)
+ }
+
+ describe '#cleanup_action_required_before_destructing?' do
+ describe 'w/ the work package having a cost entry' do
+ before do
+ work_package
+ cost_entry.save!
+ end
+
+ it 'should be true' do
+ expect(WorkPackage.cleanup_action_required_before_destructing?(work_package)).to be_truthy
+ end
+ end
+
+ describe 'w/ two work packages having a cost entry' do
+ before do
+ work_package
+ cost_entry.save!
+ cost_entry2.save!
+ end
+
+ it 'should be true' do
+ expect(WorkPackage.cleanup_action_required_before_destructing?([work_package, work_package2])).to be_truthy
+ end
+ end
+
+ describe 'w/o the work package having a cost entry' do
+ before do
+ work_package
+ end
+
+ it 'should be false' do
+ expect(WorkPackage.cleanup_action_required_before_destructing?(work_package)).to be_falsey
+ end
+ end
+ end
+
+ describe '#associated_classes_to_address_before_destructing?' do
+ describe 'w/ the work package having a cost entry' do
+ before do
+ work_package
+ cost_entry.save!
+ end
+
+ it "should be have 'CostEntry' as class to address" do
+ expect(WorkPackage.associated_classes_to_address_before_destruction_of(work_package)).to eq([CostEntry])
+ end
+ end
+
+ describe 'w/o the work package having a cost entry' do
+ before do
+ work_package
+ end
+
+ it 'should be empty' do
+ expect(WorkPackage.associated_classes_to_address_before_destruction_of(work_package)).to be_empty
+ end
+ end
+ end
+
+ describe '#cleanup_associated_before_destructing_if_required' do
+ before do
+ work_package.save!
+
+ cost_entry.save!
+ end
+
+ describe 'w/o a cleanup beeing necessary' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'reassign') }
+
+ before do
+ cost_entry.destroy
+ end
+
+ it 'should return true' do
+ expect(action).to be_truthy
+ end
+ end
+
+ describe 'w/ "destroy" as action' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'destroy') }
+
+ it 'should return true' do
+ expect(action).to be_truthy
+ end
+
+ it 'should not touch the cost_entry' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+ end
+
+ describe 'w/o an action' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user) }
+
+ it 'should return true' do
+ expect(action).to be_truthy
+ end
+
+ it 'should not touch the cost_entry' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+ end
+
+ describe 'w/ "nullify" as action' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'nullify') }
+
+ it 'should return false' do
+ expect(action).to be_falsey
+ end
+
+ it 'should not alter the work_package_id of all cost entries' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+
+ it 'should set an error on work packages' do
+ action
+
+ expect(work_package.errors.get(:base)).to eq([I18n.t(:'activerecord.errors.models.work_package.nullify_is_not_valid_for_cost_entries')])
+ end
+ end
+
+ describe 'w/ "reassign" as action
+ w/ reassigning to a valid work_package' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'reassign', reassign_to_id: work_package2.id) }
+
+ before do
+ work_package2.save!
+ role2.permissions << :edit_cost_entries
+ role2.save!
+ member2.save!
+ end
+
+ it 'should return true' do
+ expect(action).to be_truthy
+ end
+
+ it 'should set the work_package_id of all cost entries to the new work package' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package2.id)
+ end
+
+ it "should set the project_id of all cost entries to the new work package's project" do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.project_id).to eq(work_package2.project_id)
+ end
+ end
+
+ describe 'w/ "reassign" as action
+ w/ reassigning to a work_package the user is not allowed to see' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'reassign', reassign_to_id: work_package2.id) }
+
+ before do
+ work_package2.save!
+ end
+
+ it 'should return true' do
+ expect(action).to be_falsey
+ end
+
+ it 'should not alter the work_package_id of all cost entries' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+ end
+
+ describe 'w/ "reassign" as action
+ w/ reassigning to a non existing work package' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'reassign', reassign_to_id: 0) }
+
+ it 'should return true' do
+ expect(action).to be_falsey
+ end
+
+ it 'should not alter the work_package_id of all cost entries' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+ end
+
+ describe 'w/ "reassign" as action
+ w/o providing a reassignment id' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'reassign') }
+
+ it 'should return true' do
+ expect(action).to be_falsey
+ end
+
+ it 'should not alter the work_package_id of all cost entries' do
+ action
+
+ cost_entry.reload
+ expect(cost_entry.work_package_id).to eq(work_package.id)
+ end
+ end
+
+ describe 'w/ an invalid option' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, action: 'bogus') }
+
+ it 'should return false' do
+ expect(action).to be_falsey
+ end
+ end
+
+ describe 'w/ nil as invalid option' do
+ let(:action) { WorkPackage.cleanup_associated_before_destructing_if_required(work_package, user, nil) }
+
+ it 'should return false' do
+ expect(action).to be_falsey
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/models/work_package_spec.rb b/vendored-plugins/openproject-costs/spec/models/work_package_spec.rb
new file mode 100644
index 0000000000..91387ecfe1
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/models/work_package_spec.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe WorkPackage, type: :model do
+ let(:user) { FactoryGirl.create(:admin) }
+ let(:role) { FactoryGirl.create(:role) }
+ let(:project) do
+ project = FactoryGirl.create(:project_with_types)
+ project.add_member!(user, role)
+ project
+ end
+
+ let(:project2) { FactoryGirl.create(:project_with_types, types: project.types) }
+ let(:work_package) {
+ FactoryGirl.create(:work_package, project: project,
+ type: project.types.first,
+ author: user)
+ }
+ let!(:cost_entry) { FactoryGirl.create(:cost_entry, work_package: work_package, project: project, units: 3, spent_on: Date.today, user: user, comments: 'test entry') }
+ let!(:cost_object) { FactoryGirl.create(:cost_object, project: project) }
+
+ def move_to_project(work_package, project)
+ service = MoveWorkPackageService.new(work_package, user)
+
+ service.call(project)
+ end
+
+ it 'should update cost entries on move' do
+ expect(work_package.project_id).to eql project.id
+ expect(move_to_project(work_package, project2)).not_to be_falsey
+ expect(cost_entry.reload.project_id).to eql project2.id
+ end
+
+ it 'should allow to set cost_object to nil' do
+ work_package.cost_object = cost_object
+ work_package.save!
+ expect(work_package.cost_object).to eql cost_object
+
+ work_package.reload
+ work_package.cost_object = nil
+ expect { work_package.save! }.not_to raise_error
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/plugin_spec_helper.rb b/vendored-plugins/openproject-costs/spec/plugin_spec_helper.rb
new file mode 100644
index 0000000000..2bd0d14a3e
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/plugin_spec_helper.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Cost
+ module PluginSpecHelper
+ def is_member(project, user, permissions = [])
+ role = ::FactoryGirl.create(:role, permissions: permissions)
+
+ ::FactoryGirl.create(:member, project: project,
+ principal: user,
+ roles: [role])
+ user.reload
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/budgets/budget_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/budgets/budget_resource_spec.rb
new file mode 100644
index 0000000000..ded72f6c5c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/budgets/budget_resource_spec.rb
@@ -0,0 +1,107 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Budget resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
+ let(:project) { FactoryGirl.create(:project, is_public: false) }
+ let(:current_user) do
+ FactoryGirl.create(:user,
+ member_in_project: project,
+ member_through_role: role)
+ end
+ subject(:response) { last_response }
+
+ let!(:budget) { FactoryGirl.create(:cost_object, project: project) }
+
+ describe 'budgets/:id' do
+ let(:get_path) { api_v3_paths.budget budget.id }
+
+ context 'logged in user' do
+ before do
+ allow(User).to receive(:current).and_return current_user
+
+ get get_path
+ end
+
+ context 'valid id' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'invalid id' do
+ let(:get_path) { api_v3_paths.budget 'bogus' }
+
+ it_behaves_like 'not found' do
+ let(:id) { 'bogus' }
+ end
+ end
+ end
+
+ context 'not logged in user' do
+ before do
+ get get_path
+ end
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+
+ describe 'projects/:id/budgets' do
+ let(:get_path) { api_v3_paths.budgets_by_project project.id }
+
+ context 'logged in user' do
+ before do
+ allow(User).to receive(:current).and_return current_user
+
+ get get_path
+ end
+
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'not logged in user' do
+ before do
+ get get_path
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb
new file mode 100644
index 0000000000..aabd0e3b62
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entries_by_work_package_resource_spec.rb
@@ -0,0 +1,113 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Entry resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: permissions) }
+ let(:work_package_permissions) { [:view_work_packages] }
+ let(:cost_entry_permissions) { [:view_cost_entries] }
+ let(:permissions) { work_package_permissions + cost_entry_permissions }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ subject(:response) { last_response }
+
+ let(:cost_entry) {
+ FactoryGirl.build(:cost_entry,
+ project: project,
+ work_package: work_package,
+ user: current_user)
+ }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ cost_entry.save!
+
+ get get_path
+ end
+
+ describe 'work_packages/:id/cost_entries' do
+ let(:get_path) { api_v3_paths.cost_entries_by_work_package work_package.id }
+
+ context 'user can see any cost entries' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user can see own cost entries' do
+ let(:cost_entry_permissions) { [:view_own_cost_entries] }
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:cost_entry_permissions) { [] }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+
+ describe 'work_packages/:id/summarized_costs_by_type' do
+ let(:get_path) { api_v3_paths.summarized_work_package_costs_by_type work_package.id }
+
+ context 'user can see any cost entries' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user can see own cost entries' do
+ let(:cost_entry_permissions) { [:view_own_cost_entries] }
+
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:cost_entry_permissions) { [] }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entry_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entry_resource_spec.rb
new file mode 100644
index 0000000000..19aa6ce9a8
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/cost_entries/cost_entry_resource_spec.rb
@@ -0,0 +1,103 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Entry resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: permissions) }
+ let(:permissions) { [:view_cost_entries] }
+ let(:project) { FactoryGirl.create(:project) }
+ subject(:response) { last_response }
+
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project) }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ cost_entry.save!
+
+ get get_path
+ end
+
+ describe 'cost_entries/:id' do
+ let(:get_path) { api_v3_paths.cost_entry cost_entry.id }
+
+ context 'user can see cost entries' do
+ context 'valid id' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'invalid id' do
+ let(:get_path) { api_v3_paths.cost_type 'bogus' }
+
+ it_behaves_like 'not found' do
+ let(:id) { 'bogus' }
+ end
+ end
+ end
+
+ context 'user can only see own cost entries' do
+ let(:permissions) { [:view_own_cost_entries] }
+
+ context 'cost entry is not his own' do
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+
+ context 'cost entry is his own' do
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project, user: current_user) }
+
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+ end
+
+ context 'user has no cost entry permissions' do
+ let(:permissions) { [] }
+
+ describe 'he can\'t even see own cost entries' do
+ let(:cost_entry) { FactoryGirl.build(:cost_entry, project: project, user: current_user) }
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/cost_types/cost_type_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/cost_types/cost_type_resource_spec.rb
new file mode 100644
index 0000000000..740a32372c
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/cost_types/cost_type_resource_spec.rb
@@ -0,0 +1,87 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Cost Type resource' do
+ include Rack::Test::Methods
+ include API::V3::Utilities::PathHelper
+
+ let(:current_user) {
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ }
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_cost_entries]) }
+ let(:project) { FactoryGirl.create(:project) }
+ subject(:response) { last_response }
+
+ let!(:cost_type) { FactoryGirl.create(:cost_type) }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+
+ get get_path
+ end
+
+ describe 'cost_types/:id' do
+ let(:get_path) { api_v3_paths.cost_type cost_type.id }
+
+ context 'user can see cost entries' do
+ context 'valid id' do
+ it 'should return HTTP 200' do
+ expect(response.status).to eql(200)
+ end
+ end
+
+ context 'cost type deleted' do
+ let!(:cost_type) { FactoryGirl.create(:cost_type, :deleted) }
+
+ it_behaves_like 'not found' do
+ let(:id) { cost_type.id }
+ end
+ end
+
+ context 'invalid id' do
+ let(:get_path) { api_v3_paths.cost_type 'bogus' }
+
+ it_behaves_like 'not found' do
+ let(:id) { 'bogus' }
+ end
+ end
+ end
+
+ context 'user can\'t see cost entries' do
+ let(:current_user) { FactoryGirl.create(:user) }
+
+ it_behaves_like 'error response',
+ 403,
+ 'MissingPermission',
+ I18n.t('api_v3.errors.code_403')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_form_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_form_resource_spec.rb
new file mode 100644
index 0000000000..a1dfdab96d
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_form_resource_spec.rb
@@ -0,0 +1,146 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Work package form resource', type: :request do
+ include Rack::Test::Methods
+ include Capybara::RSpecMatchers
+ include API::V3::Utilities::PathHelper
+
+ let(:project) { FactoryGirl.create(:project, is_public: false) }
+ let(:work_package) { FactoryGirl.create(:work_package, project: project) }
+ let(:authorized_user) { FactoryGirl.create(:user, member_in_project: project) }
+
+ describe '#post' do
+ let(:post_path) { api_v3_paths.work_package_form work_package.id }
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ subject(:response) { last_response }
+
+ shared_context 'post request' do
+ before(:each) do
+ allow(User).to receive(:current).and_return current_user
+ post post_path, (params ? params.to_json : nil), 'CONTENT_TYPE' => 'application/json'
+ end
+ end
+
+ context 'user with needed permissions' do
+ let(:params) {}
+ let(:current_user) { authorized_user }
+
+ context 'existing work package' do
+ shared_examples_for 'having no errors' do
+ it {
+ expect(subject.body).to be_json_eql({}.to_json)
+ .at_path('_embedded/validationErrors')
+ }
+ end
+
+ shared_examples_for 'having an error' do |property|
+ it { expect(subject.body).to have_json_path("_embedded/validationErrors/#{property}") }
+
+ describe 'error body' do
+ let(:error_id) { 'urn:openproject-org:api:v3:errors:PropertyConstraintViolation' }
+
+ let(:error_body) {
+ parse_json(subject.body)['_embedded']['validationErrors'][property]
+ }
+
+ it { expect(error_body['errorIdentifier']).to eq(error_id) }
+ end
+ end
+
+ describe 'body' do
+ context 'filled' do
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ describe 'budget' do
+ let(:path) { '_embedded/payload/_links/costObject/href' }
+ let(:links_path) { '_embedded/schema/costObject/_links' }
+ let(:target_budget) { FactoryGirl.create(:cost_object, project: project) }
+ let(:other_budget) { FactoryGirl.create(:cost_object, project: project) }
+ let(:budget_link) { api_v3_paths.budget target_budget.id }
+ let(:other_budget_link) { api_v3_paths.budget other_budget.id }
+ let(:budget_parameter) { { _links: { costObject: { href: budget_link } } } }
+ let(:params) { valid_params.merge(budget_parameter) }
+
+ describe 'allowed values' do
+ before do
+ other_budget
+ end
+
+ include_context 'post request'
+
+ it 'should list the budgets' do
+ expect(subject.body).to be_json_eql(budget_link.to_json)
+ .at_path("#{links_path}/allowedValues/1/href")
+ expect(subject.body).to be_json_eql(other_budget_link.to_json)
+ .at_path("#{links_path}/allowedValues/0/href")
+ end
+ end
+
+ context 'valid budget' do
+ include_context 'post request'
+
+ it_behaves_like 'having no errors'
+
+ it 'should respond with updated work package budget' do
+ expect(subject.body).to be_json_eql(budget_link.to_json).at_path(path)
+ end
+ end
+
+ context 'invalid budget' do
+ let(:target_budget) { FactoryGirl.create(:cost_object) }
+
+ include_context 'post request'
+
+ it_behaves_like 'having an error', 'costObject'
+
+ it 'should respond with updated work package budget' do
+ expect(subject.body).to be_json_eql(budget_link.to_json).at_path(path)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_resource_spec.rb b/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_resource_spec.rb
new file mode 100644
index 0000000000..3802862a37
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/requests/api/work_packages/work_package_resource_spec.rb
@@ -0,0 +1,101 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require 'rack/test'
+
+describe 'API v3 Work package resource', type: :request do
+ include Rack::Test::Methods
+ include Capybara::RSpecMatchers
+ include API::V3::Utilities::PathHelper
+
+ let(:work_package) {
+ FactoryGirl.create(:work_package, project: project)
+ }
+
+ let(:project) do
+ FactoryGirl.create(:project, identifier: 'test_project', is_public: false)
+ end
+ let(:role) do
+ FactoryGirl.create(:role, permissions: [:view_work_packages, :edit_work_packages])
+ end
+ let(:current_user) do
+ FactoryGirl.create(:user, member_in_project: project, member_through_role: role)
+ end
+
+ describe '#patch' do
+ let(:patch_path) { api_v3_paths.work_package work_package.id }
+ let(:valid_params) do
+ {
+ _type: 'WorkPackage',
+ lockVersion: work_package.lock_version
+ }
+ end
+
+ subject(:response) { last_response }
+
+ shared_context 'patch request' do
+ before(:each) do
+ allow(User).to receive(:current).and_return current_user
+ patch patch_path, params.to_json, 'CONTENT_TYPE' => 'application/json'
+ end
+ end
+
+ context 'user with needed permissions' do
+ context 'budget' do
+ let(:target_budget) { FactoryGirl.create(:cost_object, project: project) }
+ let(:budget_link) { api_v3_paths.budget target_budget.id }
+ let(:budget_parameter) { { _links: { costObject: { href: budget_link } } } }
+ let(:params) { valid_params.merge(budget_parameter) }
+
+ before do allow(User).to receive(:current).and_return current_user end
+
+ context 'valid' do
+ include_context 'patch request'
+
+ it { expect(response.status).to eq(200) }
+
+ it 'should respond with the work package and its new budget' do
+ expect(subject.body).to be_json_eql(target_budget.subject.to_json)
+ .at_path('_embedded/costObject/subject')
+ end
+ end
+
+ context 'not valid' do
+ let(:target_budget) { FactoryGirl.create(:cost_object) }
+
+ include_context 'patch request'
+
+ it_behaves_like 'constraint violation' do
+ let(:message) { I18n.t('activerecord.errors.messages.inclusion') }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/routing/cost_objects_routing_spec.rb b/vendored-plugins/openproject-costs/spec/routing/cost_objects_routing_spec.rb
new file mode 100644
index 0000000000..6705ed2237
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/routing/cost_objects_routing_spec.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe CostObjectsController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/blubs/cost_objects/new')).to route_to(controller: 'cost_objects',
+ action: 'new',
+ project_id: 'blubs')
+ }
+ it {
+ expect(post('/projects/blubs/cost_objects')).to route_to(controller: 'cost_objects',
+ action: 'create',
+ project_id: 'blubs')
+ }
+ it {
+ expect(get('/projects/blubs/cost_objects')).to route_to(controller: 'cost_objects',
+ action: 'index',
+ project_id: 'blubs')
+ }
+ it {
+ expect(get('/cost_objects/5')).to route_to(controller: 'cost_objects',
+ action: 'show',
+ id: '5')
+ }
+ it {
+ expect(put('/cost_objects/5')).to route_to(controller: 'cost_objects',
+ action: 'update',
+ id: '5')
+ }
+ it {
+ expect(delete('/cost_objects/5')).to route_to(controller: 'cost_objects',
+ action: 'destroy',
+ id: '5')
+ }
+ it {
+ expect(post('/projects/42/cost_objects/update_material_budget_item')).to route_to(controller: 'cost_objects',
+ action: 'update_material_budget_item',
+ project_id: '42')
+ }
+ it {
+ expect(post('/projects/42/cost_objects/update_labor_budget_item')).to route_to(controller: 'cost_objects',
+ action: 'update_labor_budget_item',
+ project_id: '42')
+ }
+ it {
+ expect(get('/cost_objects/5/copy')).to route_to(controller: 'cost_objects',
+ action: 'copy',
+ id: '5')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/routing/cost_types_routing_spec.rb b/vendored-plugins/openproject-costs/spec/routing/cost_types_routing_spec.rb
new file mode 100644
index 0000000000..e35ba9fbe7
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/routing/cost_types_routing_spec.rb
@@ -0,0 +1,69 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe CostTypesController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/cost_types')).to route_to(controller: 'cost_types',
+ action: 'index')
+ }
+
+ it {
+ expect(post('/cost_types')).to route_to(controller: 'cost_types',
+ action: 'create')
+ }
+
+ it {
+ expect(get('/cost_types/new')).to route_to(controller: 'cost_types',
+ action: 'new')
+ }
+
+ it {
+ expect(get('/cost_types/5/edit')).to route_to(controller: 'cost_types',
+ action: 'edit',
+ id: '5')
+ }
+
+ it {
+ expect(put('/cost_types/5')).to route_to(controller: 'cost_types',
+ action: 'update',
+ id: '5')
+ }
+
+ it {
+ expect(put('/cost_types/5/set_rate')).to route_to(controller: 'cost_types',
+ action: 'set_rate',
+ id: '5')
+ }
+
+ it {
+ expect(delete('/cost_types/5')).to route_to(controller: 'cost_types',
+ action: 'destroy',
+ id: '5')
+ }
+
+ it {
+ expect(patch('/cost_types/5/restore')).to route_to(controller: 'cost_types',
+ action: 'restore',
+ id: '5')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/routing/costlog_routing_spec.rb b/vendored-plugins/openproject-costs/spec/routing/costlog_routing_spec.rb
new file mode 100644
index 0000000000..830ec8a0b5
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/routing/costlog_routing_spec.rb
@@ -0,0 +1,66 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe CostlogController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/work_packages/5/cost_entries')).to route_to(controller: 'costlog',
+ action: 'index',
+ work_package_id: '5')
+ }
+
+ it {
+ expect(get('/projects/blubs/cost_entries/new')).to route_to(controller: 'costlog',
+ action: 'new',
+ project_id: 'blubs')
+ }
+
+ it {
+ expect(post('/projects/blubs/cost_entries')).to route_to(controller: 'costlog',
+ action: 'create',
+ project_id: 'blubs')
+ }
+
+ it {
+ expect(get('/work_packages/5/cost_entries/new')).to route_to(controller: 'costlog',
+ action: 'new',
+ work_package_id: '5')
+ }
+
+ it {
+ expect(get('/cost_entries/5/edit')).to route_to(controller: 'costlog',
+ action: 'edit',
+ id: '5')
+ }
+
+ it {
+ expect(put('/cost_entries/5')).to route_to(controller: 'costlog',
+ action: 'update',
+ id: '5')
+ }
+
+ it {
+ expect(delete('/cost_entries/5')).to route_to(controller: 'costlog',
+ action: 'destroy',
+ id: '5')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/routing/hourly_rates_routing_spec.rb b/vendored-plugins/openproject-costs/spec/routing/hourly_rates_routing_spec.rb
new file mode 100644
index 0000000000..11ef540ed2
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/routing/hourly_rates_routing_spec.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe HourlyRatesController, type: :routing do
+ describe 'routing' do
+ it {
+ expect(get('/projects/blubs/hourly_rates/5')).to route_to(controller: 'hourly_rates',
+ action: 'show',
+ project_id: 'blubs',
+ id: '5')
+ }
+
+ it {
+ expect(get('/projects/blubs/hourly_rates/5/edit')).to route_to(controller: 'hourly_rates',
+ action: 'edit',
+ project_id: 'blubs',
+ id: '5')
+ }
+
+ it {
+ expect(get('/hourly_rates/5/edit')).to route_to(controller: 'hourly_rates',
+ action: 'edit',
+ id: '5')
+ }
+
+ it {
+ expect(put('/projects/blubs/hourly_rates/5')).to route_to(controller: 'hourly_rates',
+ action: 'update',
+ project_id: 'blubs',
+ id: '5')
+ }
+
+ it {
+ expect(post('/projects/blubs/hourly_rates/5/set_rate')).to route_to(controller: 'hourly_rates',
+ action: 'set_rate',
+ project_id: 'blubs',
+ id: '5')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-costs/spec/spec_helper.rb b/vendored-plugins/openproject-costs/spec/spec_helper.rb
new file mode 100644
index 0000000000..0d73234cef
--- /dev/null
+++ b/vendored-plugins/openproject-costs/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# -- load spec_helper from OpenProject core
+require 'spec_helper'
+
+require File.join(File.dirname(__FILE__), 'plugin_spec_helper')
diff --git a/vendored-plugins/openproject-documents/.hound.yml b/vendored-plugins/openproject-documents/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-documents/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-documents/.rubocop.yml b/vendored-plugins/openproject-documents/.rubocop.yml
new file mode 100644
index 0000000000..d32efc64ee
--- /dev/null
+++ b/vendored-plugins/openproject-documents/.rubocop.yml
@@ -0,0 +1,1007 @@
+Style/AccessModifierIndentation:
+ Description: Check indentation of private/protected visibility modifiers.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
+ Enabled: true
+
+Style/AccessorMethodName:
+ Description: Check the naming of accessor methods for get_/set_.
+ Enabled: false
+
+Style/Alias:
+ Description: 'Use alias_method instead of alias.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
+ Enabled: true
+
+Style/AlignArray:
+ Description: >-
+ Align the elements of an array literal if they span more than
+ one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
+ Enabled: true
+
+Style/AlignHash:
+ Description: >-
+ Align the elements of a hash literal if they span more than
+ one line.
+ Enabled: true
+
+Style/AlignParameters:
+ Description: >-
+ Align the parameters of a method call if they span more
+ than one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
+ Enabled: false
+
+Style/AndOr:
+ Description: 'Use &&/|| instead of and/or.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
+ Enabled: false
+
+Style/ArrayJoin:
+ Description: 'Use Array#join instead of Array#*.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
+ Enabled: false
+
+Style/AsciiComments:
+ Description: 'Use only ascii symbols in comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
+ Enabled: false
+
+Style/AsciiIdentifiers:
+ Description: 'Use only ascii symbols in identifiers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
+ Enabled: true
+
+Style/Attr:
+ Description: 'Checks for uses of Module#attr.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
+ Enabled: false
+
+Style/BeginBlock:
+ Description: 'Avoid the use of BEGIN blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
+ Enabled: true
+
+Style/BarePercentLiterals:
+ Description: 'Checks if usage of %() or %Q() matches configuration.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
+ Enabled: false
+
+Style/BlockComments:
+ Description: 'Do not use block comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
+ Enabled: false
+
+Style/BlockDelimiters:
+ Description: >-
+ Avoid using {...} for multi-line blocks (multiline chaining is
+ always ugly).
+ Prefer {...} over do...end for single-line blocks.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/BlockEndNewline:
+ Description: 'Put end statement of multiline block on its own line.'
+ Enabled: true
+
+Style/BracesAroundHashParameters:
+ Description: 'Enforce braces style around hash parameters.'
+ Enabled: false
+
+Style/CaseEquality:
+ Description: 'Avoid explicit use of the case equality operator(===).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
+ Enabled: false
+
+Style/CaseIndentation:
+ Description: 'Indentation of when in a case/when/[else/]end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
+ Enabled: true
+
+Style/CharacterLiteral:
+ Description: 'Checks for uses of character literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
+ Enabled: true
+
+Style/ClassAndModuleCamelCase:
+ Description: 'Use CamelCase for classes and modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
+ Enabled: true
+
+Style/ClassAndModuleChildren:
+ Description: 'Checks style of children classes and modules.'
+ Enabled: false
+
+Style/ClassCheck:
+ Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
+ Enabled: false
+
+Style/ClassMethods:
+ Description: 'Use self when defining module/class methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-singletons'
+ Enabled: false
+
+Style/ClassVars:
+ Description: 'Avoid the use of class variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
+ Enabled: true
+
+Style/ColonMethodCall:
+ Description: 'Do not use :: for method call.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
+ Enabled: false
+
+Style/CommentAnnotation:
+ Description: >-
+ Checks formatting of special comments
+ (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
+ Enabled: false
+
+Style/CommentIndentation:
+ Description: 'Indentation of comments.'
+ Enabled: true
+
+Style/ConstantName:
+ Description: 'Constants should use SCREAMING_SNAKE_CASE.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
+ Enabled: true
+
+Style/DefWithParentheses:
+ Description: 'Use def with parentheses when there are arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/DeprecatedHashMethods:
+ Description: 'Checks for use of deprecated Hash methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
+ Enabled: false
+
+Style/Documentation:
+ Description: 'Document classes and non-namespace modules.'
+ Enabled: false
+
+Style/DotPosition:
+ Description: 'Checks the position of the dot in multi-line method calls.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
+ Enabled: false
+
+Style/DoubleNegation:
+ Description: 'Checks for uses of double negation (!!).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
+ Enabled: false
+
+Style/EachWithObject:
+ Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
+ Enabled: false
+
+Style/ElseAlignment:
+ Description: 'Align elses and elsifs correctly.'
+ Enabled: true
+
+Style/EmptyElse:
+ Description: 'Avoid empty else-clauses.'
+ Enabled: false
+
+Style/EmptyLineBetweenDefs:
+ Description: 'Use empty lines between defs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
+ Enabled: false
+
+Style/EmptyLines:
+ Description: "Don't use several empty lines in a row."
+ Enabled: false
+
+Style/EmptyLinesAroundAccessModifier:
+ Description: "Keep blank lines around access modifiers."
+ Enabled: false
+
+Style/EmptyLinesAroundBlockBody:
+ Description: "Keeps track of empty lines around block bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundClassBody:
+ Description: "Keeps track of empty lines around class bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundModuleBody:
+ Description: "Keeps track of empty lines around module bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundMethodBody:
+ Description: "Keeps track of empty lines around method bodies."
+ Enabled: false
+
+Style/EmptyLiteral:
+ Description: 'Prefer literals to Array.new/Hash.new/String.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
+ Enabled: false
+
+Style/EndBlock:
+ Description: 'Avoid the use of END blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
+ Enabled: false
+
+Style/EndOfLine:
+ Description: 'Use Unix-style line endings.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
+ Enabled: false
+
+Style/EvenOdd:
+ Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: false
+
+Style/FileName:
+ Description: 'Use snake_case for source file names.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
+ Enabled: false
+
+Style/FlipFlop:
+ Description: 'Checks for flip flops'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
+ Enabled: false
+
+Style/For:
+ Description: 'Checks use of for or each in multiline loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
+ Enabled: false
+
+Style/FormatString:
+ Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
+ Enabled: false
+
+Style/GlobalVars:
+ Description: 'Do not introduce global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
+ Enabled: false
+
+Style/GuardClause:
+ Description: 'Check for conditionals that can be replaced with guard clauses'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/HashSyntax:
+ Description: >-
+ Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
+ { :a => 1, :b => 2 }.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
+ Enabled: true
+
+Style/IfUnlessModifier:
+ Description: >-
+ Favor modifier if/unless usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
+ Enabled: false
+
+Style/IfWithSemicolon:
+ Description: 'Do not use if x; .... Use the ternary operator instead.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
+ Enabled: false
+
+Style/IndentationConsistency:
+ Description: 'Keep indentation straight.'
+ Enabled: true
+
+Style/IndentationWidth:
+ Description: 'Use 2 spaces for indentation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/IndentArray:
+ Description: >-
+ Checks the indentation of the first element in an array
+ literal.
+ Enabled: false
+
+Style/IndentHash:
+ Description: 'Checks the indentation of the first key in a hash literal.'
+ Enabled: false
+
+Style/InfiniteLoop:
+ Description: 'Use Kernel#loop for infinite loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
+ Enabled: false
+
+Style/Lambda:
+ Description: 'Use the new lambda literal syntax for single-line blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
+ Enabled: false
+
+Style/LambdaCall:
+ Description: 'Use lambda.call(...) instead of lambda.(...).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
+ Enabled: false
+
+Style/LeadingCommentSpace:
+ Description: 'Comments should start with a space.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
+ Enabled: false
+
+Style/LineEndConcatenation:
+ Description: >-
+ Use \ instead of + or << to concatenate two string literals at
+ line end.
+ Enabled: false
+
+Style/MethodCallParentheses:
+ Description: 'Do not use parentheses for method calls with no arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
+ Enabled: false
+
+Style/MethodDefParentheses:
+ Description: >-
+ Checks if the method definitions have or don't have
+ parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/MethodName:
+ Description: 'Use the configured style when naming methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/ModuleFunction:
+ Description: 'Checks for usage of `extend self` in modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
+ Enabled: false
+
+Style/MultilineBlockChain:
+ Description: 'Avoid multi-line chains of blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/MultilineBlockLayout:
+ Description: 'Ensures newlines after multiline block do statements.'
+ Enabled: true
+
+Style/MultilineIfThen:
+ Description: 'Do not use then for multi-line if/unless.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
+ Enabled: false
+
+Style/MultilineOperationIndentation:
+ Description: >-
+ Checks indentation of binary operations that span more than
+ one line.
+ Enabled: false
+
+Style/MultilineTernaryOperator:
+ Description: >-
+ Avoid multi-line ?: (the ternary operator);
+ use if/unless instead.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
+ Enabled: false
+
+Style/NegatedIf:
+ Description: >-
+ Favor unless over if for negative conditions
+ (or control flow or).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
+ Enabled: false
+
+Style/NegatedWhile:
+ Description: 'Favor until over while for negative conditions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
+ Enabled: false
+
+Style/NestedTernaryOperator:
+ Description: 'Use one expression per branch in a ternary operator.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
+ Enabled: true
+
+Style/Next:
+ Description: 'Use `next` to skip iteration instead of a condition at the end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/NilComparison:
+ Description: 'Prefer x.nil? to x == nil.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: true
+
+Style/NonNilCheck:
+ Description: 'Checks for redundant nil checks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
+ Enabled: true
+
+Style/Not:
+ Description: 'Use ! instead of not.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
+ Enabled: true
+
+Style/NumericLiterals:
+ Description: >-
+ Add underscores to large numeric literals to improve their
+ readability.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
+ Enabled: false
+
+Style/OneLineConditional:
+ Description: >-
+ Favor the ternary operator(?:) over
+ if/then/else/end constructs.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
+ Enabled: true
+
+Style/OpMethod:
+ Description: 'When defining binary operators, name the argument other.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
+ Enabled: false
+
+Style/ParenthesesAroundCondition:
+ Description: >-
+ Don't use parentheses around the condition of an
+ if/unless/while.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
+ Enabled: true
+
+Style/PercentLiteralDelimiters:
+ Description: 'Use `%`-literal delimiters consistently'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
+ Enabled: false
+
+Style/PercentQLiterals:
+ Description: 'Checks if uses of %Q/%q match the configured preference.'
+ Enabled: false
+
+Style/PerlBackrefs:
+ Description: 'Avoid Perl-style regex back references.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
+ Enabled: false
+
+Style/PredicateName:
+ Description: 'Check the names of predicate methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
+ Enabled: false
+
+Style/Proc:
+ Description: 'Use proc instead of Proc.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
+ Enabled: false
+
+Style/RaiseArgs:
+ Description: 'Checks the arguments passed to raise/fail.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
+ Enabled: false
+
+Style/RedundantBegin:
+ Description: "Don't use begin blocks when they are not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
+ Enabled: false
+
+Style/RedundantException:
+ Description: "Checks for an obsolete RuntimeException argument in raise/fail."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
+ Enabled: false
+
+Style/RedundantReturn:
+ Description: "Don't use return where it's not required."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
+ Enabled: true
+
+Style/RedundantSelf:
+ Description: "Don't use self where it's not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
+ Enabled: false
+
+Style/RegexpLiteral:
+ Description: >-
+ Use %r for regular expressions matching more than
+ `MaxSlashes` '/' characters.
+ Use %r only for regular expressions matching more than
+ `MaxSlashes` '/' character.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
+ Enabled: false
+
+Style/RescueModifier:
+ Description: 'Avoid using rescue in its modifier form.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
+ Enabled: false
+
+Style/SelfAssignment:
+ Description: >-
+ Checks for places where self-assignment shorthand should have
+ been used.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
+ Enabled: false
+
+Style/Semicolon:
+ Description: "Don't use semicolons to terminate expressions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
+ Enabled: false
+
+Style/SignalException:
+ Description: 'Checks for proper usage of fail and raise.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
+ Enabled: false
+
+Style/SingleLineBlockParams:
+ Description: 'Enforces the names of some block params.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
+ Enabled: false
+
+Style/SingleLineMethods:
+ Description: 'Avoid single-line methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
+ Enabled: false
+
+Style/SingleSpaceBeforeFirstArg:
+ Description: >-
+ Checks that exactly one space is used between a method name
+ and the first argument for method calls without parentheses.
+ Enabled: false
+
+Style/SpaceAfterColon:
+ Description: 'Use spaces after colons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterComma:
+ Description: 'Use spaces after commas.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterControlKeyword:
+ Description: 'Use spaces after if/elsif/unless/while/until/case/when.'
+ Enabled: false
+
+Style/SpaceAfterMethodName:
+ Description: >-
+ Do not put a space between a method name and the opening
+ parenthesis in a method definition.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: false
+
+Style/SpaceAfterNot:
+ Description: Tracks redundant space after the ! operator.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
+ Enabled: false
+
+Style/SpaceAfterSemicolon:
+ Description: 'Use spaces after semicolons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeBlockBraces:
+ Description: >-
+ Checks that the left block brace has or doesn't have space
+ before it.
+ Enabled: false
+
+Style/SpaceBeforeComma:
+ Description: 'No spaces before commas.'
+ Enabled: false
+
+Style/SpaceBeforeComment:
+ Description: >-
+ Checks for missing space between code and a comment on the
+ same line.
+ Enabled: false
+
+Style/SpaceBeforeSemicolon:
+ Description: 'No spaces before semicolons.'
+ Enabled: false
+
+Style/SpaceInsideBlockBraces:
+ Description: >-
+ Checks that block braces have or don't have surrounding space.
+ For blocks taking parameters, checks that the left brace has
+ or doesn't have trailing space.
+ Enabled: false
+
+Style/SpaceAroundEqualsInParameterDefault:
+ Description: >-
+ Checks that the equals signs in parameter default assignments
+ have or don't have surrounding space depending on
+ configuration.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
+ Enabled: false
+
+Style/SpaceAroundOperators:
+ Description: 'Use spaces around operators.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeModifierKeyword:
+ Description: 'Put a space before the modifier keyword.'
+ Enabled: false
+
+Style/SpaceInsideBrackets:
+ Description: 'No spaces after [ or before ].'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideHashLiteralBraces:
+ Description: "Use spaces inside hash literal braces - or don't."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: true
+
+Style/SpaceInsideParens:
+ Description: 'No spaces after ( or before ).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideRangeLiteral:
+ Description: 'No spaces inside range literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
+ Enabled: false
+
+Style/SpecialGlobalVars:
+ Description: 'Avoid Perl-style global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
+ Enabled: false
+
+Style/StringLiterals:
+ Description: 'Checks if uses of quotes match the configured preference.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
+ Enabled: false
+
+Style/StringLiteralsInInterpolation:
+ Description: >-
+ Checks if uses of quotes inside expressions in interpolated
+ strings match the configured preference.
+ Enabled: false
+
+Style/StructInheritance:
+ Enabled: false
+
+Style/SymbolProc:
+ Description: 'Use symbols as procs instead of blocks when possible.'
+ Enabled: false
+
+Style/Tab:
+ Description: 'No hard tabs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/TrailingBlankLines:
+ Description: 'Checks trailing blank lines and final newline.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
+ Enabled: true
+
+Style/TrailingComma:
+ Description: 'Checks for trailing comma in parameter lists and literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
+ Enabled: false
+
+Style/TrailingWhitespace:
+ Description: 'Avoid trailing whitespace.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
+ Enabled: false
+
+Style/TrivialAccessors:
+ Description: 'Prefer attr_* methods to trivial readers/writers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
+ Enabled: false
+
+Style/UnlessElse:
+ Description: >-
+ Do not use unless with else. Rewrite these with the positive
+ case first.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
+ Enabled: false
+
+Style/UnneededCapitalW:
+ Description: 'Checks for %W when interpolation is not needed.'
+ Enabled: false
+
+Style/UnneededPercentQ:
+ Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
+ Enabled: false
+
+Style/VariableInterpolation:
+ Description: >-
+ Don't interpolate global, instance and class variables
+ directly in strings.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
+ Enabled: false
+
+Style/VariableName:
+ Description: 'Use the configured style when naming variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/WhenThen:
+ Description: 'Use when x then ... for one-line cases.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
+ Enabled: false
+
+Style/WhileUntilDo:
+ Description: 'Checks for redundant do after while or until.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
+ Enabled: false
+
+Style/WhileUntilModifier:
+ Description: >-
+ Favor modifier while/until usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
+ Enabled: false
+
+Style/WordArray:
+ Description: 'Use %w or %W for arrays of words.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
+ Enabled: false
+
+#################### Metrics ################################
+
+Metrics/AbcSize:
+ Description: >-
+ A calculated magnitude based on number of assignments,
+ branches, and conditions.
+ Enabled: false
+
+Metrics/BlockNesting:
+ Description: 'Avoid excessive block nesting'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
+ Enabled: false
+
+Metrics/ClassLength:
+ Description: 'Avoid classes longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/ModuleLength:
+ Description: 'Avoid modules longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/CyclomaticComplexity:
+ Description: >-
+ A complexity metric that is strongly correlated to the number
+ of test cases needed to validate a method.
+ Enabled: false
+
+Metrics/LineLength:
+ Description: 'Limit lines to 80 characters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
+ Enabled: true
+ Max: 120
+
+Metrics/MethodLength:
+ Description: 'Avoid methods longer than 10 lines of code.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
+ Enabled: false
+
+Metrics/ParameterLists:
+ Description: 'Avoid parameter lists longer than three or four parameters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
+ Enabled: false
+
+Metrics/PerceivedComplexity:
+ Description: >-
+ A complexity metric geared towards measuring complexity for a
+ human reader.
+ Enabled: false
+
+#################### Lint ################################
+### Warnings
+
+Lint/AmbiguousOperator:
+ Description: >-
+ Checks for ambiguous operators in the first argument of a
+ method invocation without parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
+ Enabled: false
+
+Lint/AmbiguousRegexpLiteral:
+ Description: >-
+ Checks for ambiguous regexp literals in the first argument of
+ a method invocation without parenthesis.
+ Enabled: false
+
+Lint/AssignmentInCondition:
+ Description: "Don't use assignment in conditions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
+ Enabled: false
+
+Lint/BlockAlignment:
+ Description: 'Align block ends correctly.'
+ Enabled: false
+
+Lint/ConditionPosition:
+ Description: >-
+ Checks for condition placed in a confusing position relative to
+ the keyword.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
+ Enabled: false
+
+Lint/Debugger:
+ Description: 'Check for debugger calls.'
+ Enabled: true
+
+Lint/DefEndAlignment:
+ Description: 'Align ends corresponding to defs correctly.'
+ Enabled: false
+
+Lint/DeprecatedClassMethods:
+ Description: 'Check for deprecated class method calls.'
+ Enabled: false
+
+Lint/ElseLayout:
+ Description: 'Check for odd code arrangement in an else block.'
+ Enabled: false
+
+Lint/EmptyEnsure:
+ Description: 'Checks for empty ensure block.'
+ Enabled: false
+
+Lint/EmptyInterpolation:
+ Description: 'Checks for empty string interpolation.'
+ Enabled: false
+
+Lint/EndAlignment:
+ Description: 'Align ends correctly.'
+ Enabled: false
+
+Lint/EndInMethod:
+ Description: 'END blocks should not be placed inside method definitions.'
+ Enabled: false
+
+Lint/EnsureReturn:
+ Description: 'Do not use return in an ensure block.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
+ Enabled: false
+
+Lint/Eval:
+ Description: 'The use of eval represents a serious security risk.'
+ Enabled: false
+
+Lint/HandleExceptions:
+ Description: "Don't suppress exception."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
+ Enabled: false
+
+Lint/InvalidCharacterLiteral:
+ Description: >-
+ Checks for invalid character literals with a non-escaped
+ whitespace character.
+ Enabled: false
+
+Lint/LiteralInCondition:
+ Description: 'Checks of literals used in conditions.'
+ Enabled: false
+
+Lint/LiteralInInterpolation:
+ Description: 'Checks for literals used in interpolation.'
+ Enabled: false
+
+Lint/Loop:
+ Description: >-
+ Use Kernel#loop with break rather than begin/end/until or
+ begin/end/while for post-loop tests.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
+ Enabled: false
+
+Lint/NestedMethodDefinition:
+ Description: 'Do not use nested method definitions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods'
+ Enabled: false
+
+Lint/NonLocalExitFromIterator:
+ Description: 'Do not use return in iterator to cause non-local exit.'
+ Enabled: true
+
+Lint/ParenthesesAsGroupedExpression:
+ Description: >-
+ Checks for method calls with a space before the opening
+ parenthesis.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: true
+
+Lint/RequireParentheses:
+ Description: >-
+ Use parentheses in the method call to avoid confusion
+ about precedence.
+ Enabled: false
+
+Lint/RescueException:
+ Description: 'Avoid rescuing the Exception class.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
+ Enabled: true
+
+Lint/ShadowingOuterLocalVariable:
+ Description: >-
+ Do not use the same name as outer local variable
+ for block arguments or block local variables.
+ Enabled: false
+
+Lint/SpaceBeforeFirstArg:
+ Description: >-
+ Put a space between a method name and the first argument
+ in a method call without parentheses.
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Description: 'Checks for Object#to_s usage in string interpolation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
+ Enabled: true
+
+Lint/UnderscorePrefixedVariableName:
+ Description: 'Do not use prefix `_` for a variable that is used.'
+ Enabled: true
+
+Lint/UnusedBlockArgument:
+ Description: 'Checks for unused block arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnusedMethodArgument:
+ Description: 'Checks for unused method arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnreachableCode:
+ Description: 'Unreachable code.'
+ Enabled: true
+
+Lint/UselessAccessModifier:
+ Description: 'Checks for useless access modifiers.'
+ Enabled: false
+
+Lint/UselessAssignment:
+ Description: 'Checks for useless assignment to a local variable.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UselessComparison:
+ Description: 'Checks for comparison of something with itself.'
+ Enabled: false
+
+Lint/UselessElseWithoutRescue:
+ Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
+ Enabled: true
+
+Lint/UselessSetterCall:
+ Description: 'Checks for useless setter call to a local variable.'
+ Enabled: false
+
+Lint/Void:
+ Description: 'Possible use of operator/literal/variable in void context.'
+ Enabled: false
+
+##################### Rails ##################################
+
+Rails/ActionFilter:
+ Description: 'Enforces consistent use of action filter methods.'
+ Enabled: true
+
+Rails/DefaultScope:
+ Description: 'Checks if the argument passed to default_scope is a block.'
+ Enabled: false
+
+Rails/Delegate:
+ Description: 'Prefer delegate method for delegations.'
+ Enabled: false
+
+Rails/HasAndBelongsToMany:
+ Description: 'Prefer has_many :through to has_and_belongs_to_many.'
+ Enabled: false
+
+Rails/Output:
+ Description: 'Checks for calls to puts, print, etc.'
+ Enabled: true
+
+Rails/ReadWriteAttribute:
+ Description: >-
+ Checks for read_attribute(:attr) and
+ write_attribute(:attr, val).
+ Enabled: false
+
+Rails/ScopeArgs:
+ Description: 'Checks the arguments of ActiveRecord scopes.'
+ Enabled: false
+
+Rails/Validation:
+ Description: 'Use validates :attribute, hash of validations.'
+ Enabled: false
+
+AllCops:
+ RunRailsCops: true
+ Exclude:
+ - 'vendor/**/*'
+ - 'db/**/*'
+ - 'tmp/**/*'
+ - 'bin/**/*'
diff --git a/vendored-plugins/openproject-documents/.travis.yml b/vendored-plugins/openproject-documents/.travis.yml
new file mode 100644
index 0000000000..991de401b8
--- /dev/null
+++ b/vendored-plugins/openproject-documents/.travis.yml
@@ -0,0 +1,111 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Travis configuration based on the respective OpenProject core configuration.
+# Everything save for the matrix section and additional `before_install`
+# instructions is copied and pasted from the core.
+
+language: ruby
+
+rvm:
+ - 2.2.3
+
+sudo: false
+
+cache:
+ - bundler: true
+ - directories:
+ - frontend/node_modules
+ - frontend/bower_components
+
+bundler_args: --without development production
+
+branches:
+ only:
+ - master
+ - dev
+ - /^(stable|release)\/.*$/
+
+env:
+ global:
+ - CI=true
+ - RAILS_ENV=test
+ - COVERAGE=true
+
+ matrix:
+ - "TEST_SUITE=plugins:spec DB=mysql"
+ - "TEST_SUITE=plugins:cucumber DB=mysql"
+
+before_install:
+ # Custom plugin instructions follow.
+
+ # Move the plugin into a subfolder. The plugin-provided Gemfile.plugins
+ # must refer to this folder.
+ - mkdir -p plugins/this
+ - echo `ls -a | tail -n+3 | grep -v plugins` plugins/this/ | xargs mv
+
+ # Get OpenProject.
+ # Doing the fetch detour as you cannot clone into the current directory.
+ - git init
+ - git remote add openproject https://github.com/opf/openproject.git
+ - git fetch --depth=1 openproject
+ - git checkout openproject/stable/5
+
+ # End of custom plugin instructions.
+
+ - "echo `firefox -v`"
+ - "export DISPLAY=:99.0"
+ - "/sbin/start-stop-daemon --start -v --pidfile ./tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x16"
+ - "echo `xdpyinfo -display :99 | grep 'dimensions' | awk '{ print $2 }'`"
+ - travis_retry npm install
+
+ # We need phantomjs 2.0 to get tests passing
+ - mkdir travis-phantomjs
+ - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2
+ - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs
+ - export PATH=$PWD/travis-phantomjs:$PATH
+
+before_script:
+ - sh script/ci_setup.sh $DB
+
+script:
+ - sh script/ci_runner.sh $TEST_SUITE $GROUP_SIZE $GROUP
+
+notifications:
+ email: false
+ slack:
+ secure: "a+I0uMgXgrDd3aitr2yhXrh7g/UOUTwoDVElunY7gYdrM+gpZ6RE1AP4/Q++hERBCs7rUBzmb//zxGTcc8Nw4nGqZOmPOMIsAoD49UupGLUzHbxzKlpwdBcwh77fq3rYwkjZjE/H1qiElPT7v6qyWMSdNGlj/bAB74eD7Zl3S5cMRvZ1whbSg2GA2v6ZqtXaKfrSFrPRzsIOBXs99OxWNWAsUGpEwTYac7wb6rdMJkBbzosp4gP99mGvQArEzo0nrIQgRH8W4Q6iLnrpX0g5uKccWl1u/G2bmH8L4F50ce4uuUE+TtHO/nfNFnb2KuDR4QyoccQQbGHXL/jaaAZXG/gzs5Hmru2Thaym43fSwxos80xmZs1vqB/rXE+Rg9qXcCKyyX31zjSI/iW4wS015fz8MKVX6qDg49epaw1ovn0AOYrvTd94GV6RX6eJ3/l+KJJHSKaaLP/713h11LWx/S27tiB40fboXQ68YzIQCuahRUEHUfhU3P10Wf9y2fdDsthtHHSrOJMQ3Ii/Jm3KQm6bE5RWORdHvc/sF2WLfLmJ627j9JhWYYi5mDKJ9AeMWtZNHreU0mM27OUgfhiW11ItKgpwQPEiiicrlYRrMmK+9hc9cym+8tRM+wEth1xhIkfgQFtngONKjv361Wt3JifxM79+bn0IyF72vAVNy8k="
+
+addons:
+ firefox: "38.0esr"
+ postgresql: "9.3"
+
+# Disabling coverage reporting until CodeClimate supports merging results from multiple partial tests
+# code_climate:
+# repo_token:
+# secure: "W/lyd8Ud18GRASuVShsIKa2MRHhxjh8WICMQ4WKr68qt0X0Tlp7Bclv4ReiEgiQeKsIoJJy5FfJfINdAT8A4sy2JbrLeISShcIU7Kqpfh6DSLNoRAuLz5P7EXMNFns1gBKCmrSzcB+9ksuTLyTCKkjUcj1NbJzGqpB4jSTecAdg="
diff --git a/vendored-plugins/openproject-documents/Gemfile.plugins b/vendored-plugins/openproject-documents/Gemfile.plugins
new file mode 100644
index 0000000000..39f9d179f0
--- /dev/null
+++ b/vendored-plugins/openproject-documents/Gemfile.plugins
@@ -0,0 +1,7 @@
+# Used by travis to bundle this plugin with the OpenProject core.
+# The tested plugin will be moved to the path `./plugins/this`
+# whereas OpenProject will be checked out to `.`.
+
+gem 'openproject-documents', path: 'plugins/this'
+
+# If the plugin has any dependencies declare them here:
diff --git a/vendored-plugins/openproject-documents/README.md b/vendored-plugins/openproject-documents/README.md
new file mode 100644
index 0000000000..d99aef77e2
--- /dev/null
+++ b/vendored-plugins/openproject-documents/README.md
@@ -0,0 +1,105 @@
+OpenProject Documents Plugin
+===========================
+
+This plugin adds features to connect and categorize documents with your project.
+
+Under `Modules >> Administration >> Enumerations` you can find the section `Document categories`
+where you can define several document categories that projects can use to categorize their documents.
+
+Documents can be enabled for every project individually. Simply activate the `Documents` module in the project settings.
+
+When you go to any of your projects you can see the entry `Documents` in the main menu. There you can
+attach new documents to the project by following the `New document` link located in the top right corner of the page.
+
+The form allows you to select one of the categories you defined earlier, choose a title and define a description.
+You can attach files from your local hard disk to the document entry which will make them available to anybody
+who has access to the document.
+
+Requirements
+------------
+
+The OpenProject Documents plug-in requires the [OpenProject Core](https://github.com/opf/openproject/) in version greater or equal to *3.0.0*.
+
+
+Installation
+------------
+
+You need to add a line to the `Gemfile.plugins` of OpenProject, specifying the OpenProject version you are using (whereas 'stable/X.Y' specifies the OpenProject version you are using):
+
+`gem "openproject-documents", git: "https://github.com/opf/openproject-documents.git", :branch => "stable/X.Y"`
+
+*Example:*
+To install the Documents-plugin for 'OpenProject 4.1', add the following line to the `Gemfile.plugins` of OpenProject:
+
+`gem "openproject-documents", git: "https://github.com/opf/openproject-documents.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+
+Tests
+-----
+
+Assuming you have to following directory structure:
+
+```
+.
+├── openproject
+├── openproject-documents
+```
+
+Replace the openproject-document ``Gemfile.plugins`` entry with the following:
+
+```
+gem "openproject-documents", path: "../openproject-documents"
+```
+
+You run the specs with the following commands:
+
+```
+cd openproject
+rake db:test:load # this needs to be done only once
+rspec ../openproject-documents
+```
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-documents", git: "https://github.com/opf/openproject-documents.git", :branch => "stable"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this leaves plugin data in the database. Currently, we do not support full uninstall of the plugin.
+
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/documents
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/opf/openproject-documents`
+
+Licence
+-------
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-documents/app/assets/stylesheets/documents/documents.css.erb b/vendored-plugins/openproject-documents/app/assets/stylesheets/documents/documents.css.erb
new file mode 100644
index 0000000000..d67d8d44f4
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/assets/stylesheets/documents/documents.css.erb
@@ -0,0 +1,35 @@
+/*-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++*/
+
+dt.document:before {
+ content: "\e006";
+}
diff --git a/vendored-plugins/openproject-documents/app/controllers/documents_controller.rb b/vendored-plugins/openproject-documents/app/controllers/documents_controller.rb
new file mode 100644
index 0000000000..4a8d39a530
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/controllers/documents_controller.rb
@@ -0,0 +1,117 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class DocumentsController < ApplicationController
+ default_search_scope :documents
+ model_object Document
+ before_action :find_project_by_project_id, only: [:index, :new, :create]
+ before_action :find_model_object, except: [:index, :new, :create]
+ before_action :find_project_from_association, except: [:index, :new, :create]
+ before_action :authorize
+
+
+ def index
+ @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
+ documents = @project.documents
+ case @sort_by
+ when 'date'
+ @grouped = documents.group_by {|d| d.updated_on.to_date }
+ when 'title'
+ @grouped = documents.group_by {|d| d.title.first.upcase}
+ when 'author'
+ @grouped = documents.with_attachments.group_by {|d| d.attachments.last.author}
+ else
+ @grouped = documents.includes(:category).group_by(&:category)
+ end
+ render layout: false if request.xhr?
+ end
+
+ def show
+ @attachments = @document.attachments.order('created_on DESC')
+ end
+
+ def new
+ @document = @project.documents.build
+ @document.attributes = document_params
+ end
+
+ def create
+ @document = @project.documents.build
+ @document.attributes = document_params
+ if @document.save
+ Attachment.attach_files(@document, params[:attachments])
+ render_attachment_warning_if_needed(@document)
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to project_documents_path(@project)
+ else
+ render action: 'new'
+ end
+ end
+
+ def edit
+ @categories = DocumentCategory.all
+ end
+
+ def update
+ @document.attributes = document_params
+ if @document.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to action: 'show', id: @document
+ else
+ render action: 'edit'
+ end
+ end
+
+ def destroy
+ @document.destroy
+ redirect_to controller: '/documents', action: 'index', project_id: @project
+ end
+
+ def add_attachment
+ attachments = Attachment.attach_files(@document, params[:attachments])
+ render_attachment_warning_if_needed(@document)
+
+ if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
+ users = attachments[:files].first.container.recipients
+ users.each do |user|
+ UserMailer.attachments_added(user, attachments[:files]).deliver
+ end
+ end
+ redirect_to action: 'show', id: @document
+ end
+
+ private
+
+ def document_params
+ params.fetch(:document, {}).permit('category_id', 'title', 'description')
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/mailers/documents_mailer.rb b/vendored-plugins/openproject-documents/app/mailers/documents_mailer.rb
new file mode 100644
index 0000000000..efe049a3c8
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/mailers/documents_mailer.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class DocumentsMailer < UserMailer
+
+ def document_added(user, document)
+ @document = document
+
+ open_project_headers 'Project' => @document.project.identifier,
+ 'Type' => 'Document'
+
+ with_locale_for(user) do
+ subject = "[#{@document.project.name}] #{t(:label_document_new)}: #{@document.title}"
+ mail to: user.mail, subject: subject
+ end
+ end
+
+ def attachments_added(user, attachments)
+ container = attachments.first.container
+
+ @added_to = "#{Document.model_name.human}: #{container.title}"
+ @added_to_url = url_for(controller: '/documents', action: 'show', id: container.id)
+
+ super
+ end
+
+end
diff --git a/vendored-plugins/openproject-documents/app/models/activity/document_activity_provider.rb b/vendored-plugins/openproject-documents/app/models/activity/document_activity_provider.rb
new file mode 100644
index 0000000000..6ac38efcb3
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/activity/document_activity_provider.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Activity::DocumentActivityProvider < Activity::BaseActivityProvider
+ acts_as_activity_provider type: 'documents',
+ permission: :view_documents
+
+ def event_query_projection(activity)
+ [
+ activity_journal_projection_statement(:title, 'document_title', activity),
+ activity_journal_projection_statement(:project_id, 'project_id', activity)
+ ]
+ end
+
+ def event_title(event, _activity)
+ "#{Document.model_name.human}: #{event['document_title']}"
+ end
+
+ def event_type(_event, _activity)
+ 'document'
+ end
+
+ def event_path(event, _activity)
+ url_helpers.project_documents_url(url_helper_parameter(event))
+ end
+
+ def event_url(event, _activity)
+ url_helpers.project_documents_url(url_helper_parameter(event))
+ end
+
+ private
+
+ def url_helper_parameter(event)
+ event['journable_id']
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/models/document.rb b/vendored-plugins/openproject-documents/app/models/document.rb
new file mode 100644
index 0000000000..c110f2b5a4
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/document.rb
@@ -0,0 +1,86 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Document < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :category, class_name: "DocumentCategory", foreign_key: "category_id"
+ acts_as_attachable delete_permission: :manage_documents
+
+ acts_as_journalized
+ acts_as_event title: Proc.new { |o| "#{Document.model_name.human}: #{o.title}" },
+ url: Proc.new { |o| { controller: '/documents', action: 'show', id: o.id } },
+ datetime: :created_on,
+ author: ( Proc.new do |o|
+ o.attachments.find(:first, order: "#{Attachment.table_name}.created_on ASC").try(:author)
+ end)
+
+
+ acts_as_searchable columns: ['title', "#{table_name}.description"],
+ include: :project,
+ references: :projects
+
+ validates_presence_of :project, :title, :category
+ validates_length_of :title, maximum: 60
+
+ scope :visible, lambda {
+ includes(:project)
+ .where(Project.allowed_to_condition(User.current, :view_documents))
+ .references(:projects)
+ }
+
+ scope :with_attachments, lambda {
+ includes(:attachments)
+ .where('attachments.container_id is not NULL')
+ .references(:attachments)
+ }
+
+ after_initialize :set_default_category
+
+ def visible?(user=User.current)
+ !user.nil? && user.allowed_to?(:view_documents, project)
+ end
+
+ def set_default_category
+ self.category ||= DocumentCategory.default if new_record?
+ end
+
+ def updated_on
+ unless @updated_on
+ # attachments has a default order that conflicts with `created_on DESC`
+ # #reorder removes that default order but rather than #unscoped keeps the
+ # scoping by this document
+ a = attachments.reorder('created_on DESC').first
+ @updated_on = (a && a.created_on) || created_on
+ end
+ @updated_on
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/models/document_category.rb b/vendored-plugins/openproject-documents/app/models/document_category.rb
new file mode 100644
index 0000000000..2ff7f3fe43
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/document_category.rb
@@ -0,0 +1,49 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class DocumentCategory < Enumeration
+ has_many :documents, foreign_key: 'category_id'
+
+ OptionName = :enumeration_doc_categories
+
+ def option_name
+ OptionName
+ end
+
+ def objects_count
+ documents.count
+ end
+
+ def transfer_relations(to)
+ documents.update_all("category_id = #{to.id}")
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/models/document_category_custom_field.rb b/vendored-plugins/openproject-documents/app/models/document_category_custom_field.rb
new file mode 100644
index 0000000000..6c4a945477
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/document_category_custom_field.rb
@@ -0,0 +1,37 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class DocumentCategoryCustomField < CustomField
+ def type_name
+ :enumeration_doc_categories
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/models/document_observer.rb b/vendored-plugins/openproject-documents/app/models/document_observer.rb
new file mode 100644
index 0000000000..5a5254bd70
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/document_observer.rb
@@ -0,0 +1,41 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class DocumentObserver < ActiveRecord::Observer
+ def after_create(document)
+ return unless Setting.notified_events.include?('document_added')
+
+ document.recipients.each do |user|
+ DocumentsMailer.document_added(user, document).deliver_now
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-documents/app/models/journal/document_journal.rb b/vendored-plugins/openproject-documents/app/models/journal/document_journal.rb
new file mode 100644
index 0000000000..cbe8f7a798
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/models/journal/document_journal.rb
@@ -0,0 +1,35 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class Journal::DocumentJournal < Journal::BaseJournal
+ self.table_name = "document_journals"
+end
diff --git a/vendored-plugins/openproject-documents/app/views/documents/_document.html.erb b/vendored-plugins/openproject-documents/app/views/documents/_document.html.erb
new file mode 100644
index 0000000000..54226e0e07
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/_document.html.erb
@@ -0,0 +1,38 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= link_to h(document.title), :controller => '/documents', :action => 'show', :id => document %>
+<%= format_time(document.updated_on) %>
+
+
+ <%= format_text(truncate_lines(document.description)) %>
+
diff --git a/vendored-plugins/openproject-documents/app/views/documents/_form.html.erb b/vendored-plugins/openproject-documents/app/views/documents/_form.html.erb
new file mode 100644
index 0000000000..efbb345d22
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/_form.html.erb
@@ -0,0 +1,44 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= error_messages_for 'document' %>
+
+ <%= f.select :category_id, DocumentCategory.all.map { |c| [c.name, c.id] }, required: true %>
+
+
+ <%= f.text_field :title, required: true %>
+
+
+ <%= f.text_area :description %>
+
+
+<%= wikitoolbar_for 'document_description' %>
diff --git a/vendored-plugins/openproject-documents/app/views/documents/edit.html.erb b/vendored-plugins/openproject-documents/app/views/documents/edit.html.erb
new file mode 100644
index 0000000000..98a9871beb
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/edit.html.erb
@@ -0,0 +1,38 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= toolbar title: Document.model_name.human %>
+
+<%= labelled_tabular_form_for @document, url: document_path(@document), method: 'PATCH' do |f| -%>
+ <%= render partial: "documents/form", locals: { f: f } %>
+ <%= styled_button_tag l(:button_save), class: "-highlight -with-icon icon-checkmark" %>
+<% end %>
diff --git a/vendored-plugins/openproject-documents/app/views/documents/index.html.erb b/vendored-plugins/openproject-documents/app/views/documents/index.html.erb
new file mode 100644
index 0000000000..788bca0cf2
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/index.html.erb
@@ -0,0 +1,74 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= toolbar title: l(:label_document_plural) do %>
+ <% if authorize_for(:documents, :new) %>
+
+ <%= link_to({:controller => '/documents', :action => 'new', :project_id => @project}, class: 'button -alt-highlight') do %>
+ <%= l(:label_document_new) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+<% if @grouped.empty? %>
+ <%= l(:label_no_data) %>
+<% end %>
+
+<% @grouped.keys.sort.each do |group| %>
+ <%= group %>
+ <%= render :partial => 'documents/document', :collection => @grouped[group] %>
+<% end %>
+
+<% content_for :sidebar do %>
+ <%= l(:label_sort_by, '') %>
+ <%= form_tag({}, :method => :get) do %>
+
+ <%= radio_button_tag 'sort_by', 'category', (@sort_by == 'category'), :onclick => 'this.form.submit();' %>
+ <%= Document.human_attribute_name(:category) %>
+
+
+ <%= radio_button_tag 'sort_by', 'date', (@sort_by == 'date'), :onclick => 'this.form.submit();' %>
+ <%= l(:label_date) %>
+
+
+ <%= radio_button_tag 'sort_by', 'title', (@sort_by == 'title'), :onclick => 'this.form.submit();' %>
+ <%= Document.human_attribute_name(:title) %>
+
+
+ <%= radio_button_tag 'sort_by', 'author', (@sort_by == 'author'), :onclick => 'this.form.submit();' %>
+ <%= Document.human_attribute_name(:author) %>
+
+ <% end %>
+<% end %>
+
+<% html_title(l(:label_document_plural)) -%>
diff --git a/vendored-plugins/openproject-documents/app/views/documents/new.html.erb b/vendored-plugins/openproject-documents/app/views/documents/new.html.erb
new file mode 100644
index 0000000000..cfbf47f8c2
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/new.html.erb
@@ -0,0 +1,40 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+<%= toolbar title: l(:label_document_new) %>
+
+<%= labelled_tabular_form_for @document, url: project_documents_path(@project), html: { multipart: true } do |f| -%>
+ <%= render :partial => 'documents/form', locals: { f: f } %>
+ <%= render :partial => 'attachments/form' %>
+ <%= styled_button_tag l(:button_create), class: "-highlight -with-icon icon-checkmark" %>
+<% end %>
+
+
diff --git a/vendored-plugins/openproject-documents/app/views/documents/show.html.erb b/vendored-plugins/openproject-documents/app/views/documents/show.html.erb
new file mode 100644
index 0000000000..8e0e390d6b
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents/show.html.erb
@@ -0,0 +1,70 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% html_title h(@document.title) -%>
+<%= toolbar title: @document.title,
+ subtitle: "#{@document.category.name} - #{format_date @document.created_on}" do %>
+ <% if authorize_for(:documents, :edit) %>
+
+ <%= link_to({:controller => '/documents', :action => 'edit', :id => @document}, class: 'button', accesskey: accesskey(:edit)) do %>
+ <%= l(:button_edit) %>
+ <% end %>
+
+ <% end %>
+ <% if authorize_for(:documents, :destroy) %>
+
+ <%= link_to({:controller => '/documents', :action => 'destroy', :id => @document}, class: 'button', data: { :confirm => l(:text_are_you_sure) }, :method => :delete) do %>
+ <%= l(:button_delete) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
+<%= textilizable @document.description, :attachments => @document.attachments %>
+
+
+<%= l(:label_attachment_plural) %>
+<%= link_to_attachments @document %>
+
+<% if authorize_for('documents', 'add_attachment') %>
+<%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
+ :id => 'attach_files_link' %>
+ <%= form_tag(add_attachment_document_path(@document), method: :post, multipart: true, :id => "add_attachment_form", :style => "display:none;") do %>
+
+
<%= render :partial => 'attachments/form' %>
+
+ <%= styled_button_tag l(:button_add), class: "-highlight -with-icon icon-checkmark" %>
+
+ <% end %>
+<% end %>
+
diff --git a/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.html.erb b/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.html.erb
new file mode 100644
index 0000000000..89c8f349de
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.html.erb
@@ -0,0 +1,35 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= link_to(@document.title, project_documents_url(@document)) %> (<%= @document.category.name %>)
+
+<%= format_text @document.description %>
diff --git a/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.text.erb b/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.text.erb
new file mode 100644
index 0000000000..dfe6d94c69
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/documents_mailer/document_added.text.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= @document.title %> (<%= @document.category.name %>)
+<%= project_documents_url(@document) %>
+
+<%= @document.description %>
diff --git a/vendored-plugins/openproject-documents/app/views/help/wiki_syntax_detailed.html.erb b/vendored-plugins/openproject-documents/app/views/help/wiki_syntax_detailed.html.erb
new file mode 100644
index 0000000000..d43658067d
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/help/wiki_syntax_detailed.html.erb
@@ -0,0 +1,296 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<% content_for :styles do %>
+ body { font:80% Verdana,Tahoma,Arial,sans-serif; }
+ h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
+ pre, code { font-size:120%; }
+ pre code { font-size:100%; }
+ pre {
+ margin: 1em 1em 1em 1.6em;
+ padding: 2px;
+ background-color: #fafafa;
+ border: 1px solid #dadada;
+ width:95%;
+ overflow-x: auto;
+ }
+ a.new { color: #b73535; }
+
+ .CodeRay .c { color:#666; }
+
+ .CodeRay .cl { color:#B06; font-weight:bold }
+ .CodeRay .dl { color:black }
+ .CodeRay .fu { color:#06B; font-weight:bold }
+
+ .CodeRay .il { background: #eee }
+ .CodeRay .il .idl { font-weight: bold; color: #888 }
+
+ .CodeRay .iv { color:#33B }
+ .CodeRay .r { color:#080; font-weight:bold }
+
+ .CodeRay .s { background-color:#fff0f0 }
+ .CodeRay .s .dl { color:#710 }
+<% end %>
+
+<% html_title "Wiki Formatting" %>
+ Wiki Formatting
+
+ Links
+
+ OpenProject links
+
+ OpenProject allows hyperlinking between issues, changesets and wiki pages from anywhere wiki formatting is used.
+
+ #124 displays a link to an issue: #124 (link is striked-through if the issue is closed)
+ ##124 displays a link to an issue with context information: #12 New: Issue subject 2012-05-14 - 2012-05-23 (User Name - assigned to)
+ ###124 displays a link to an issue with context information and an excerpt (first 3 lines) of the description
+ r758 displays a link to a changeset: r758
+ commit:c6f4d0fd displays a link to a changeset with a non-numeric hash: c6f4d0fd
+ sandbox:r758 displays a link to a changeset of another project: sandbox:r758
+ sandbox:c6f4d0fd displays a link to a changeset with a non-numeric hash: sandbox:c6f4d0fd
+
+
+ Wiki links:
+
+
+ [[Guide]] displays a link to the page named 'Guide': Guide
+ [[Guide#further-reading]] takes you to the anchor "further-reading". Headings get automatically assigned anchors so that you can refer to them: Guide
+ [[Guide|User manual]] displays a link to the same page but with a different text: User manual
+
+
+ You can also link to pages of an other project wiki:
+
+
+ [[sandbox:some page]] displays a link to the page named 'Some page' of the Sandbox wiki
+ [[sandbox:]] displays a link to the Sandbox wiki main page
+
+
+ Wiki links are displayed in red if the page doesn't exist yet, eg: Nonexistent page .
+
+ Links to other resources:
+
+
+ Documents:
+
+ document#17 (link to document with id 17)
+ document:Greetings (link to the document with title "Greetings")
+ document:"Some document" (double quotes can be used when document title contains spaces)
+ sandbox:document:"Some document" (link to a document with title "Some document" in other project "sandbox")
+
+
+
+
+ Versions:
+
+ version#3 (link to version with id 3)
+ version:1.0.0 (link to version named "1.0.0")
+ version:"1.0 beta 2"
+ sandbox:version:1.0.0 (link to version "1.0.0" in the project "sandbox")
+
+
+
+
+ Attachments:
+
+ attachment:file.zip (link to the attachment of the current object named file.zip)
+ For now, attachments of the current object can be referenced only (if you're on an issue, it's possible to reference attachments of this issue only)
+
+
+
+
+ Repository files:
+
+ source:some/file (link to the file located at /some/file in the project's repository)
+ source:some/file@52 (link to the file's revision 52)
+ source:some/file#L120 (link to line 120 of the file)
+ source:some/file@52#L120 (link to line 120 of the file's revision 52)
+ source:"some file@52#L120" (use double quotes when the URL contains spaces
+ export:some/file (force the download of the file)
+ sandbox:source:some/file (link to the file located at /some/file in the repository of the project "sandbox")
+ sandbox:export:some/file (force the download of the file)
+
+
+
+
+ Forum messages:
+
+ message#1218 (link to message with id 1218)
+
+
+
+
+ Projects:
+
+ project#3 (link to project with id 3)
+ project:someproject (link to project named "someproject")
+
+
+
+
+ Escaping:
+
+
+ You can prevent OpenProject links from being parsed by preceding them with an exclamation mark: !
+
+
+
+ External links
+
+ HTTP URLs and email addresses are automatically turned into clickable links:
+
+
+https://www.openproject.org, someone@foo.bar
+
+
+ displays: https://www.openproject.org , someone@foo.bar
+
+ If you want to display a specific text instead of the URL, you can use the standard textile syntax:
+
+
+"OpenProject web site":https://www.openproject.org
+
+
+ displays: OpenProject web site
+
+
+ Text formatting
+
+
+ For things such as headlines, bold, tables, lists, OpenProject supports Textile syntax. See http://www.textism.com/tools/textile/ for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.
+
+ Font style
+
+
+* *bold*
+* _italic_
+* _*bold italic*_
+* +underline+
+* -strike-through-
+
+
+ Display:
+
+
+ bold
+ italic
+ *bold italic*
+ underline
+ strike-through
+
+
+ Inline images
+
+
+ !image_url! displays an image located at image_url (textile syntax)
+ !>image_url! right floating image
+ If you have an image attached to your wiki page, it can be displayed inline using its filename: !attached_image.png!
+
+
+ Headings
+
+
+h1. Heading
+h2. Subheading
+h3. Subsubheading
+
+
+ OpenProject assigns an anchor to each of those headings thus you can link to them with "#Heading", "#Subheading" and so forth.
+
+
+ Paragraphs
+
+
+p>. right aligned
+p=. centered
+
+
+ This is a centered paragraph.
+
+
+ Blockquotes
+
+ Start the paragraph with bq.
+
+
+bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
+To go live, all you need to add is a database and a web server.
+
+
+ Display:
+
+
+ Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern. To go live, all you need to add is a database and a web server.
+
+
+
+ Table of content
+
+
+{{toc}} => left aligned toc
+{{>toc}} => right aligned toc
+
+
+ Macros
+
+ OpenProject has the following builtin macros:
+
+
hello_world
Sample macro.
include
Include a wiki page. Example:
+
+ {{include(Foo)}}
macro_list
Displays a list of all available macros, including description if available.
+
+
+ Code highlighting
+
+ Code highlightment relies on CodeRay , a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.
+
+ You can highlight code in your wiki page using this syntax:
+
+
+<pre><code class="ruby">
+ Place you code here.
+</code></pre>
+
+
+ Example:
+
+ 1 # The Greeter class
+ 2 class Greeter
+ 3 def initialize (name)
+ 4 @name = name.capitalize
+ 5 end
+ 6
+ 7 def salute
+ 8 puts " Hello #{ @name } ! "
+ 9 end
+10 end
+
+
diff --git a/vendored-plugins/openproject-documents/app/views/hooks/documents/_activity_index_head.html.erb b/vendored-plugins/openproject-documents/app/views/hooks/documents/_activity_index_head.html.erb
new file mode 100644
index 0000000000..1d14dbbf23
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/hooks/documents/_activity_index_head.html.erb
@@ -0,0 +1,33 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%= stylesheet_link_tag 'documents/documents.css' %>
diff --git a/vendored-plugins/openproject-documents/app/views/my/blocks/_documents.html.erb b/vendored-plugins/openproject-documents/app/views/my/blocks/_documents.html.erb
new file mode 100644
index 0000000000..3fbdbf1028
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/my/blocks/_documents.html.erb
@@ -0,0 +1,45 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+
+<%=l(:label_document_plural)%>
+
+<% if defined? block_name %>
+ <%= content_for "#{block_name}_remove_block" %>
+<% end %>
+
+<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %>
+<%= render(partial: 'documents/document',
+ collection: Document
+ .limit(10)
+ .order("#{Document.table_name}.created_on DESC")
+ .where("#{Document.table_name}.project_id in (#{project_ids.join(',')})")
+ .includes(:project)) unless project_ids.empty? %>
diff --git a/vendored-plugins/openproject-documents/app/views/my_projects_overviews/blocks/_documents.html.erb b/vendored-plugins/openproject-documents/app/views/my_projects_overviews/blocks/_documents.html.erb
new file mode 100644
index 0000000000..03562ef2b4
--- /dev/null
+++ b/vendored-plugins/openproject-documents/app/views/my_projects_overviews/blocks/_documents.html.erb
@@ -0,0 +1,37 @@
+<%#-- copyright
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
+
+++#%>
+<%=l(:label_document_plural)%>
+
+<% if @user.allowed_to?(:view_documents, @project)%>
+ <%= render(:partial => 'documents/document',
+ :collection => Document.where(project_id: @project.id).limit(10).order("created_on DESC")) %>
+<% end %>
diff --git a/vendored-plugins/openproject-documents/config/locales/bg.yml b/vendored-plugins/openproject-documents/config/locales/bg.yml
new file mode 100644
index 0000000000..54ad6e99e0
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/bg.yml
@@ -0,0 +1,14 @@
+bg:
+ activerecord:
+ models:
+ document: Документ
+ default_doc_category_tech: Техническа документация
+ default_doc_category_user: Документация за потребителя
+ enumeration_doc_categories: Document categories
+ label_document_added: Документът е добавен
+ label_document_new: Нов документ
+ label_document_plural: Документи
+ label_documents: Документи
+ permission_manage_documents: Управление на документите
+ permission_view_documents: Преглед на документите
+ project_module_documents: Документи
diff --git a/vendored-plugins/openproject-documents/config/locales/da.yml b/vendored-plugins/openproject-documents/config/locales/da.yml
new file mode 100644
index 0000000000..c0181b2aa9
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/da.yml
@@ -0,0 +1,14 @@
+da:
+ activerecord:
+ models:
+ document: Dokument
+ default_doc_category_tech: Teknisk dokumentation
+ default_doc_category_user: Brugerdokumentation
+ enumeration_doc_categories: Dokumentkategorier
+ label_document_added: Dokument tilføjet
+ label_document_new: Nyt dokument
+ label_document_plural: Dokumenter
+ label_documents: Dokumenter
+ permission_manage_documents: Håndter dokumenter
+ permission_view_documents: Se dokumenter
+ project_module_documents: Dokumenter
diff --git a/vendored-plugins/openproject-documents/config/locales/de.yml b/vendored-plugins/openproject-documents/config/locales/de.yml
new file mode 100644
index 0000000000..2dab814780
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/de.yml
@@ -0,0 +1,14 @@
+de:
+ activerecord:
+ models:
+ document: Dokument
+ default_doc_category_tech: Technische Dokumentation
+ default_doc_category_user: Benutzerdokumentation
+ enumeration_doc_categories: Dokumentenkategorien
+ label_document_added: Dokument hinzugefügt
+ label_document_new: Neues Dokument
+ label_document_plural: Dokumente
+ label_documents: Dokumente
+ permission_manage_documents: Dokumente verwalten
+ permission_view_documents: Dokumente ansehen
+ project_module_documents: Dokumente
diff --git a/vendored-plugins/openproject-documents/config/locales/en.yml b/vendored-plugins/openproject-documents/config/locales/en.yml
new file mode 100644
index 0000000000..4f73496115
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/en.yml
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+en:
+ activerecord:
+ models:
+ document: "Document"
+
+ default_doc_category_tech: "Technical documentation"
+ default_doc_category_user: "User documentation"
+
+ enumeration_doc_categories: "Document categories"
+
+ label_document_added: "Document added"
+ label_document_new: "New document"
+ label_document_plural: "Documents"
+ label_documents: "Documents"
+
+ permission_manage_documents: "Manage documents"
+ permission_view_documents: "View documents"
+ project_module_documents: "Documents"
diff --git a/vendored-plugins/openproject-documents/config/locales/es-ES.yml b/vendored-plugins/openproject-documents/config/locales/es-ES.yml
new file mode 100644
index 0000000000..0e2a520dfd
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/es-ES.yml
@@ -0,0 +1,14 @@
+es:
+ activerecord:
+ models:
+ document: Documento
+ default_doc_category_tech: Documentación técnica
+ default_doc_category_user: Documentacion para el usuario
+ enumeration_doc_categories: Categorías de documentos
+ label_document_added: Documento agregado
+ label_document_new: Nuevo documento
+ label_document_plural: Documentos
+ label_documents: Documentos
+ permission_manage_documents: Administrar documentos
+ permission_view_documents: Ver documentos
+ project_module_documents: Documentos
diff --git a/vendored-plugins/openproject-documents/config/locales/et.yml b/vendored-plugins/openproject-documents/config/locales/et.yml
new file mode 100644
index 0000000000..2f791fe55a
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/et.yml
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+et:
+ activerecord:
+ models:
+ document: "Dokument"
+
+ default_doc_category_tech: "Tehniline dokumentatsioon"
+ default_doc_category_user: "Kasutajate dokumentatsioon"
+
+ enumeration_doc_categories: "Dokumentide kategooriad"
+
+ label_document_added: "Dokument on lisatud"
+ label_document_new: "Uus dokument"
+ label_document_plural: "Dokumendid"
+ label_documents: "Dokumendid"
+
+ permission_manage_documents: "Dokumentide haldamine"
+ permission_view_documents: "Dokumentide vaatamine"
+ project_module_documents: "Dokumendid"
diff --git a/vendored-plugins/openproject-documents/config/locales/fr.yml b/vendored-plugins/openproject-documents/config/locales/fr.yml
new file mode 100644
index 0000000000..c5175ef792
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/fr.yml
@@ -0,0 +1,14 @@
+fr:
+ activerecord:
+ models:
+ document: Document
+ default_doc_category_tech: Documentation technique
+ default_doc_category_user: Documentation utilisateur
+ enumeration_doc_categories: Catégories de documents
+ label_document_added: Document ajouté
+ label_document_new: Nouveau document
+ label_document_plural: Documents
+ label_documents: Documents
+ permission_manage_documents: Gérer les documents
+ permission_view_documents: Consulter les documents
+ project_module_documents: Documents
diff --git a/vendored-plugins/openproject-documents/config/locales/it.yml b/vendored-plugins/openproject-documents/config/locales/it.yml
new file mode 100644
index 0000000000..6147c07d4f
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/it.yml
@@ -0,0 +1,14 @@
+it:
+ activerecord:
+ models:
+ document: Documento
+ default_doc_category_tech: Documentazione tecnica
+ default_doc_category_user: Documentazione utente
+ enumeration_doc_categories: Categorie di documenti
+ label_document_added: Documento aggiunto
+ label_document_new: Nuovo Documento
+ label_document_plural: Documenti
+ label_documents: Documenti
+ permission_manage_documents: Gestione documenti
+ permission_view_documents: Visualizzare i documenti
+ project_module_documents: Documenti
diff --git a/vendored-plugins/openproject-documents/config/locales/pl.yml b/vendored-plugins/openproject-documents/config/locales/pl.yml
new file mode 100644
index 0000000000..10f0c2c86b
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/pl.yml
@@ -0,0 +1,14 @@
+pl:
+ activerecord:
+ models:
+ document: Document
+ default_doc_category_tech: Dokumentacja techniczna
+ default_doc_category_user: Dokumentacja użytkownika
+ enumeration_doc_categories: Kategoria dokumentu
+ label_document_added: Dodano dokument
+ label_document_new: Nowy dokument
+ label_document_plural: Dokumenty
+ label_documents: Dokumenty
+ permission_manage_documents: Zarządzanie dokumentami
+ permission_view_documents: Zobacz dokumenty
+ project_module_documents: Dokumenty
diff --git a/vendored-plugins/openproject-documents/config/locales/pt-BR.yml b/vendored-plugins/openproject-documents/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..f5a9804352
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/pt-BR.yml
@@ -0,0 +1,14 @@
+pt-BR:
+ activerecord:
+ models:
+ document: Documento
+ default_doc_category_tech: Documentação técnica
+ default_doc_category_user: Documentação do usuário
+ enumeration_doc_categories: Categorias de documento
+ label_document_added: Documento adicionado
+ label_document_new: Novo documento
+ label_document_plural: Documentos
+ label_documents: Documentos
+ permission_manage_documents: Gerenciar documentos
+ permission_view_documents: Visualizar documentos
+ project_module_documents: Documentos
diff --git a/vendored-plugins/openproject-documents/config/locales/ru.yml b/vendored-plugins/openproject-documents/config/locales/ru.yml
new file mode 100644
index 0000000000..276c06077a
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/ru.yml
@@ -0,0 +1,14 @@
+ru:
+ activerecord:
+ models:
+ document: Документ
+ default_doc_category_tech: Техническая документация
+ default_doc_category_user: Документация пользователя
+ enumeration_doc_categories: Категории документов
+ label_document_added: Документ добавлен
+ label_document_new: Новый документ
+ label_document_plural: Документы
+ label_documents: Документы
+ permission_manage_documents: Управление документами
+ permission_view_documents: Просмотр документов
+ project_module_documents: Документы
diff --git a/vendored-plugins/openproject-documents/config/locales/sk.yml b/vendored-plugins/openproject-documents/config/locales/sk.yml
new file mode 100644
index 0000000000..de15ef90bd
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/sk.yml
@@ -0,0 +1,14 @@
+sk:
+ activerecord:
+ models:
+ document: Dokument
+ default_doc_category_tech: Technická dokumentácia
+ default_doc_category_user: Používateľská dokumentácia
+ enumeration_doc_categories: Kategórie dokumentu
+ label_document_added: Dokument pridaný
+ label_document_new: Nový dokument
+ label_document_plural: Dokumenty
+ label_documents: Dokumenty
+ permission_manage_documents: Správa dokumentov
+ permission_view_documents: Zobrazenie dokumentov
+ project_module_documents: Dokumenty
diff --git a/vendored-plugins/openproject-documents/config/locales/sv-SE.yml b/vendored-plugins/openproject-documents/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..24ea48bb91
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/sv-SE.yml
@@ -0,0 +1,14 @@
+sv:
+ activerecord:
+ models:
+ document: Dokument
+ default_doc_category_tech: Teknisk dokumentation
+ default_doc_category_user: Användardokumentation
+ enumeration_doc_categories: Dokumentkategorier
+ label_document_added: Dokumentet tillagt
+ label_document_new: Nytt dokument
+ label_document_plural: Dokument
+ label_documents: Dokument
+ permission_manage_documents: Hantera dokument
+ permission_view_documents: Visa dokument
+ project_module_documents: Dokument
diff --git a/vendored-plugins/openproject-documents/config/locales/tr.yml b/vendored-plugins/openproject-documents/config/locales/tr.yml
new file mode 100644
index 0000000000..b5044a40ca
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/locales/tr.yml
@@ -0,0 +1,14 @@
+tr:
+ activerecord:
+ models:
+ document: Belge
+ default_doc_category_tech: Teknik belgeler
+ default_doc_category_user: Kullanıcı belgeleri
+ enumeration_doc_categories: Belge kategorileri
+ label_document_added: Belge eklendi
+ label_document_new: Yeni belge
+ label_document_plural: Belgeler
+ label_documents: Belgeler
+ permission_manage_documents: Belgeleri yönetme
+ permission_view_documents: Belgeleri görüntüleme
+ project_module_documents: Belgeler
diff --git a/vendored-plugins/openproject-documents/config/routes.rb b/vendored-plugins/openproject-documents/config/routes.rb
new file mode 100644
index 0000000000..5ac4f0d706
--- /dev/null
+++ b/vendored-plugins/openproject-documents/config/routes.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+OpenProject::Application.routes.draw do
+
+ resources :projects do
+ resources :documents, shallow: true do
+ member do
+ post 'add_attachment'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-documents/db/migrate/20130807085604_create_document_journals.rb b/vendored-plugins/openproject-documents/db/migrate/20130807085604_create_document_journals.rb
new file mode 100644
index 0000000000..6a257846fc
--- /dev/null
+++ b/vendored-plugins/openproject-documents/db/migrate/20130807085604_create_document_journals.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class CreateDocumentJournals < ActiveRecord::Migration
+ def change
+ create_table :document_journals do |t|
+ t.integer :journal_id, :null => false
+ t.integer :project_id, :default => 0, :null => false
+ t.integer :category_id, :default => 0, :null => false
+ t.string :title, :limit => 60, :default => "", :null => false
+ t.text :description
+ t.datetime :created_on
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-documents/db/migrate/20130814131242_create_documents_tables.rb b/vendored-plugins/openproject-documents/db/migrate/20130814131242_create_documents_tables.rb
new file mode 100644
index 0000000000..07384415bb
--- /dev/null
+++ b/vendored-plugins/openproject-documents/db/migrate/20130814131242_create_documents_tables.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+class CreateDocumentsTables < ActiveRecord::Migration
+ def up
+ unless ActiveRecord::Base.connection.table_exists? 'documents'
+ create_table "documents" do |t|
+ t.integer "project_id", :default => 0, :null => false
+ t.integer "category_id", :default => 0, :null => false
+ t.string "title", :limit => 60, :default => "", :null => false
+ t.text "description"
+ t.datetime "created_on"
+ end
+ add_index "documents", ["category_id"], :name => "index_documents_on_category_id"
+ add_index "documents", ["created_on"], :name => "index_documents_on_created_on"
+ add_index "documents", ["project_id"], :name => "documents_project_id"
+ end
+ end
+
+ def down
+ drop_table :documents
+ end
+end
diff --git a/vendored-plugins/openproject-documents/db/migrate/20140320140001_legacy_document_journal_data.rb b/vendored-plugins/openproject-documents/db/migrate/20140320140001_legacy_document_journal_data.rb
new file mode 100644
index 0000000000..1bd681729d
--- /dev/null
+++ b/vendored-plugins/openproject-documents/db/migrate/20140320140001_legacy_document_journal_data.rb
@@ -0,0 +1,36 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2011-2013 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+#
+
+require Rails.root.join("db","migrate","migration_utils","legacy_journal_migrator").to_s
+
+class LegacyDocumentJournalData < ActiveRecord::Migration
+ def up
+ migrator.run
+ end
+
+ def down
+ migrator.remove_journals_derived_from_legacy_journals 'document_journals'
+ end
+
+ def migrator
+ @migrator ||= Migration::LegacyJournalMigrator.new "DocumentJournal", "document_journals"
+ end
+end
diff --git a/vendored-plugins/openproject-documents/doc/CHANGELOG.md b/vendored-plugins/openproject-documents/doc/CHANGELOG.md
new file mode 100644
index 0000000000..ab4a51fba3
--- /dev/null
+++ b/vendored-plugins/openproject-documents/doc/CHANGELOG.md
@@ -0,0 +1,69 @@
+
+
+# Changelog
+
+## 3.0.8
+
+* `#7679` Fix: Permissions not nested in 'documents' project module
+* `#5650` Fix: Acts as event datetime
+
+## 1.0.1
+
+* `#5361` Add missing journal data migration
+
+## 1.0.0
+
+* `#3329` Refactor Duplicated Code Journals
+* `#5190` Public Release Documents plugin
+* Adaptations for new icon font
+
+## 1.0.0.pre5
+
+* `#2402` Activated migrations running from the plugin
+* Additional namespacing of hook partials to prevent clashes on activity view
+
+## 1.0.0.pre4
+
+* `#1875` Added document block to my project page
+
+## 1.0.0.pre3
+
+* Removed hard wiring to specific core version
+
+## 1.0.0.pre2
+
+* `#1602` Adaptions to Core acts_as_journalized_changes
+
+## 1.0.0.pre1
+
+* `#1631` Initial creation of Documents plugin
diff --git a/vendored-plugins/openproject-documents/doc/COPYRIGHT.md b/vendored-plugins/openproject-documents/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..06e4c258cf
--- /dev/null
+++ b/vendored-plugins/openproject-documents/doc/COPYRIGHT.md
@@ -0,0 +1,63 @@
+OpenProject Documents Plugin
+
+This plugin adds features to connect and categorize documents with your project.
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+---
+
+OpenProject is a derivative work based on ChiliProject, whose Copyright follows.
+
+ChiliProject is a project management system.
+
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+---
+
+ChiliProject is a derivative work based on Redmine, whose Copyright follows.
+
+Redmine - project management software
+Copyright (C) 2006-2013 Jean-Philippe Lang
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-documents/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-documents/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..889081eeb1
--- /dev/null
+++ b/vendored-plugins/openproject-documents/doc/COPYRIGHT_short.md
@@ -0,0 +1,28 @@
+OpenProject Documents Plugin
+
+Former OpenProject Core functionality extracted into a plugin.
+
+Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+Copyright (C) 2006-2013 Jean-Philippe Lang
+Copyright (C) 2010-2013 the ChiliProject Team
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.rdoc for more details.
diff --git a/vendored-plugins/openproject-documents/doc/GPL.txt b/vendored-plugins/openproject-documents/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-documents/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-documents/features/documents_widget.feature b/vendored-plugins/openproject-documents/features/documents_widget.feature
new file mode 100644
index 0000000000..5491971151
--- /dev/null
+++ b/vendored-plugins/openproject-documents/features/documents_widget.feature
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Feature: Adding the document widget to personalisable pages
+
+ Background:
+ Given there is 1 project with the following:
+ | name | project1 |
+ And I am already Admin
+
+ @javascript
+ Scenario: Adding a "Documents" widget to the my project page
+ Given the plugin "openproject_my_project_page" is loaded
+ And I am on the project "project1" overview personalization page
+ And I should see "Add" within "#block-select"
+ When I select "Documents" from "block-select"
+ Then the "Documents" widget should be in the hidden block
+ And "Documents" should be disabled in the my project page available widgets drop down
+
+ @javascript
+ Scenario: Adding a "Documents" widget to the my page
+ Given I am on the My page personalization page
+ # Safeguard to ensure the page is loaded
+ And I should see "Reported work packages"
+ When I select "Documents" from the available widgets drop down
+ And I click on "Add"
+ Then the "Documents" widget should be in the top block
+ And "Documents" should be disabled in the my page available widgets drop down
diff --git a/vendored-plugins/openproject-documents/features/support/documents_steps.rb b/vendored-plugins/openproject-documents/features/support/documents_steps.rb
new file mode 100644
index 0000000000..eb6c48759d
--- /dev/null
+++ b/vendored-plugins/openproject-documents/features/support/documents_steps.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+Given /^the [Pp]roject "([^\"]*)" has (\d+) [Dd]ocument with(?: the following)?:$/ do |project, count, table|
+ p = Project.find_by_name(project) || Project.find_by_identifier(project)
+ as_admin count do
+ d = Document.spawn
+ d.project = p
+ d.category = DocumentCategory.first
+ d.save!
+ send_table_to_object(d, table)
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents.rb b/vendored-plugins/openproject-documents/lib/open_project/documents.rb
new file mode 100644
index 0000000000..0e82d37d2e
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module Documents
+ require "open_project/documents/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/engine.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/engine.rb
new file mode 100644
index 0000000000..61e557173f
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/engine.rb
@@ -0,0 +1,87 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+
+module OpenProject::Documents
+ class Engine < ::Rails::Engine
+ engine_name :openproject_documents
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-documents',
+ author_url: "http://www.openproject.com",
+ requires_openproject: ">= 4.0.0" do
+
+ menu :project_menu, :documents,
+ { controller: '/documents', action: 'index' },
+ param: :project_id,
+ caption: :label_document_plural,
+ html: { class: 'icon2 icon-notes' }
+
+ project_module :documents do |_map|
+ permission :manage_documents, {
+ documents: [:new, :create, :edit, :update, :destroy, :add_attachment]
+ }, require: :loggedin
+ permission :view_documents, documents: [:index, :show, :download]
+ end
+
+ Redmine::Notifiable.all << Redmine::Notifiable.new('document_added')
+
+ Redmine::Activity.map do |activity|
+ activity.register :documents, class_name: 'Activity::DocumentActivityProvider', default: false
+ end
+
+ Redmine::Search.register :documents
+ end
+
+ patches [:CustomFieldsHelper, :Project]
+
+ assets %w(documents.css)
+
+ initializer "documents.register_hooks" do
+ require 'open_project/documents/hooks'
+ end
+
+ initializer 'documents.register_observers' do |_app|
+ ActiveRecord::Base.observers.push :document_observer
+ end
+
+ config.to_prepare do
+ require_dependency 'document_category'
+ require_dependency 'document_category_custom_field'
+
+ # Have to apply this one by hand and not via op_engine patches method
+ # becauses the op_engine method does not allow for patching something
+ # in the lib/open_project directory. Bummer.
+ require_dependency 'open_project/documents/patches/text_formatting_patch'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/hooks.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/hooks.rb
new file mode 100644
index 0000000000..e38c642cf3
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/hooks.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Documents
+ class Hooks < Redmine::Hook::ViewListener
+ render_on :activity_index_head,
+ partial: 'hooks/documents/activity_index_head'
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/patches.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/patches.rb
new file mode 100644
index 0000000000..c4bff1a671
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/patches.rb
@@ -0,0 +1,35 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Documents
+ module Patches
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/patches/custom_fields_helper_patch.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/custom_fields_helper_patch.rb
new file mode 100644
index 0000000000..11122cb279
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/custom_fields_helper_patch.rb
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Documents::Patches
+ module CustomFieldsHelperPatch
+ def self.included(base)
+
+ base.class_eval do
+
+ def custom_fields_tabs_with_documents
+ custom_fields_tabs_without_documents << {
+ name: 'DocumentCategoryCustomField',
+ partial: 'custom_fields/tab',
+ label: DocumentCategory::OptionName
+ }
+ end
+
+ alias_method_chain :custom_fields_tabs, :documents
+ end
+
+ end
+
+ end
+end
+
+unless CustomFieldsHelper.included_modules.include?(OpenProject::Documents::Patches::CustomFieldsHelperPatch)
+ CustomFieldsHelper.send(:include, OpenProject::Documents::Patches::CustomFieldsHelperPatch)
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/patches/project_patch.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/project_patch.rb
new file mode 100644
index 0000000000..934b1a3f47
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/project_patch.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Documents::Patches
+ module ProjectPatch
+ def self.included(base)
+ base.class_eval do
+
+ has_many :documents, dependent: :destroy
+ end
+ end
+ end
+end
+
+Project.send(:include, OpenProject::Documents::Patches::ProjectPatch)
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/patches/text_formatting_patch.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/text_formatting_patch.rb
new file mode 100644
index 0000000000..d42447a9fc
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/patches/text_formatting_patch.rb
@@ -0,0 +1,83 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject::Documents::Patches
+ module TextFormattingPatch
+ def self.included(base)
+
+ base.class_eval do
+
+ def parse_redmine_links_with_documents(text, project, obj, attr, only_path, options)
+ text.gsub!(/([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(document)((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)/) do |_m|
+ leading = $1
+ esc = $2
+ project_prefix = $3
+ project_identifier = $4
+ prefix = $5
+ sep = $7 || $9
+ identifier = $8 || $10
+ link = nil
+ if project_identifier
+ project = Project.visible.find_by_identifier(project_identifier)
+ end
+ if esc.nil?
+ if sep == '#'
+ oid = identifier.to_i
+ document = Document.visible.find_by_id(oid)
+ elsif sep == ':' && project
+ name = identifier.gsub(%r{^"(.*)"$}, "\\1")
+ document = project.documents.visible.find_by_title(name)
+ end
+ if document
+ link = link_to document.title, {
+ only_path: only_path,
+ controller: '/documents',
+ action: 'show', id: document },
+ class: 'document'
+ end
+ end
+ leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
+ end
+
+ parse_redmine_links_without_documents(text, project, obj, attr, only_path, options)
+ end
+
+ alias_method_chain :parse_redmine_links, :documents
+ end
+
+ end
+
+ end
+end
+
+unless OpenProject::TextFormatting.included_modules.include?(OpenProject::Documents::Patches::TextFormattingPatch)
+ OpenProject::TextFormatting.send(:include, OpenProject::Documents::Patches::TextFormattingPatch)
+end
diff --git a/vendored-plugins/openproject-documents/lib/open_project/documents/version.rb b/vendored-plugins/openproject-documents/lib/open_project/documents/version.rb
new file mode 100644
index 0000000000..98a0281371
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/open_project/documents/version.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ module Documents
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-documents/lib/openproject-documents.rb b/vendored-plugins/openproject-documents/lib/openproject-documents.rb
new file mode 100644
index 0000000000..4472b8ebc9
--- /dev/null
+++ b/vendored-plugins/openproject-documents/lib/openproject-documents.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'open_project/documents'
diff --git a/vendored-plugins/openproject-documents/openproject-documents.gemspec b/vendored-plugins/openproject-documents/openproject-documents.gemspec
new file mode 100644
index 0000000000..413971167c
--- /dev/null
+++ b/vendored-plugins/openproject-documents/openproject-documents.gemspec
@@ -0,0 +1,24 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/documents/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-documents"
+ s.version = OpenProject::Documents::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/documents"
+ s.summary = "OpenProject Documents"
+ s.description = "An OpenProject plugin to allow creation of documents in projects"
+ s.license = "GPLv3"
+
+ s.files = Dir["{app,config,db,lib,doc}/**/*", "README.md"]
+ s.test_files = Dir["spec/**/*"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_development_dependency 'rspec-rails'
+ s.add_development_dependency 'cucumber-rails'
+ s.add_development_dependency 'database_cleaner'
+end
diff --git a/vendored-plugins/openproject-documents/spec/application_helper_spec.rb b/vendored-plugins/openproject-documents/spec/application_helper_spec.rb
new file mode 100644
index 0000000000..79e31b6b19
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/application_helper_spec.rb
@@ -0,0 +1,128 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe ApplicationHelper do
+ include ApplicationHelper
+ include ActionView::Helpers
+ include ActionDispatch::Routing
+ include Rails.application.routes.url_helpers
+
+
+ describe ".format_text" do
+ let(:project) { FactoryGirl.create :valid_project }
+ let(:identifier) { project.identifier }
+ let(:role) {
+ FactoryGirl.create(:role, permissions: [
+ :view_work_packages, :edit_work_packages, :view_documents, :browse_repository, :view_changesets, :view_wiki_pages
+ ])
+ }
+ let(:project_member) {
+ FactoryGirl.create :user, member_in_project: project,
+ member_through_role: role
+ }
+ let(:document) {
+ FactoryGirl.create :document,
+ title: 'Test document',
+ project: project
+ }
+
+ before do
+ @project = project
+ allow(User).to receive(:current).and_return project_member
+ end
+
+ after do
+ allow(User).to receive(:current).and_call_original
+ end
+
+ context "Simple Document links" do
+ let(:document_link) {
+ link_to('Test document',
+ { controller: 'documents', action: 'show', id: document.id },
+ class: 'document')
+ }
+
+ context "Plain link" do
+ subject { format_text("document##{document.id}") }
+
+ it { is_expected.to eq("#{document_link}
") }
+ end
+
+ context "Link with document name" do
+ subject { format_text("document##{document.id}") }
+
+ it { is_expected.to eq("#{document_link}
") }
+ end
+
+ context "Escaping plain link" do
+ subject { format_text("!document##{document.id}") }
+
+ it { is_expected.to eq("document##{document.id}
") }
+ end
+
+ context "Escaping link with document name" do
+ subject { format_text('!document:"Test document"') }
+
+ it { is_expected.to eq('document:"Test document"
') }
+ end
+ end
+
+ context 'Cross-Project Document Links' do
+ let(:the_other_project) { FactoryGirl.create :valid_project }
+
+ context "By name without project" do
+ subject { format_text("document:\"#{document.title}\"", project: the_other_project) }
+
+ it { is_expected.to eq('document:"Test document"
') }
+ end
+
+ context "By id and given project" do
+ subject { format_text("#{identifier}:document##{document.id}", project: the_other_project) }
+
+ it { is_expected.to eq("Test document
") }
+ end
+
+ context "By name and given project" do
+ subject { format_text("#{identifier}:document:\"#{document.title}\"", project: the_other_project) }
+
+ it { is_expected.to eq("Test document
") }
+ end
+
+ context "Invalid link" do
+ subject { format_text("invalid:document:\"Test document\"", project: the_other_project) }
+
+ it { is_expected.to eq('invalid:document:"Test document"
') }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/assets/attachments/testfile.txt b/vendored-plugins/openproject-documents/spec/assets/attachments/testfile.txt
new file mode 100644
index 0000000000..84f601ee68
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/assets/attachments/testfile.txt
@@ -0,0 +1,2 @@
+this is a text file for upload tests
+with multiple lines
diff --git a/vendored-plugins/openproject-documents/spec/controllers/documents_controller_spec.rb b/vendored-plugins/openproject-documents/spec/controllers/documents_controller_spec.rb
new file mode 100644
index 0000000000..b849c096eb
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/controllers/documents_controller_spec.rb
@@ -0,0 +1,209 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe DocumentsController do
+
+ render_views
+
+ let(:admin) { FactoryGirl.create(:admin)}
+ let(:project) { FactoryGirl.create(:project, name: "Test Project")}
+ let(:user) { FactoryGirl.create(:user)}
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_documents]) }
+
+ let(:default_category){
+ FactoryGirl.create(:document_category, project: project, name: "Default Category")
+ }
+
+ let(:document) {
+ FactoryGirl.create(:document, title: "Sample Document", project: project, category: default_category)
+ }
+
+ before do
+ allow(User).to receive(:current).and_return admin
+ end
+
+ describe "index" do
+
+ before do
+ document.update_attributes(description:< { description: "sample file", file: file_attachment } }
+ end
+
+ it "should add an attachment" do
+ document = Document.last
+
+ expect(document.attachments.count).to eql 1
+ attachment = document.attachments.first
+ expect(attachment.description).to eql "sample file"
+ expect(attachment.filename).to eql "testfile.txt"
+ end
+
+ it "should redirect to the documents-page" do
+ expect(response).to redirect_to project_documents_path(project.identifier)
+ end
+
+ it "should send out mails with notifications to members of the project with :view_documents-permission" do
+ expect(ActionMailer::Base.deliveries.size).to eql 1
+ end
+ end
+ end
+
+ describe 'show' do
+ before do
+ document
+ get :show, id: document.id
+ end
+
+ it "should delete the document and redirect back to documents-page of the project" do
+ expect(response).to be_success
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe '#add_attachment' do
+ before do
+ document
+ post :add_attachment,
+ id: document.id,
+ attachments: { '1' => { description: "sample file", file: file_attachment } }
+ end
+
+ it "should delete the document and redirect back to documents-page of the project" do
+ expect(response).to be_redirect
+ document.reload
+ expect(document.attachments.length).to eq(1)
+ end
+ end
+
+ describe "destroy" do
+ before do
+ document
+ end
+
+ it "should delete the document and redirect back to documents-page of the project" do
+ expect{
+ delete :destroy, id: document.id
+ }.to change{Document.count}.by -1
+
+ expect(response).to redirect_to "/projects/#{project.identifier}/documents"
+ expect{Document.find(document.id)}.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+
+ def file_attachment
+ test_document = "#{OpenProject::Documents::Engine.root}/spec/assets/attachments/testfile.txt"
+ Rack::Test::UploadedFile.new(test_document, "text/plain")
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/factories/document_category_factory.rb b/vendored-plugins/openproject-documents/spec/factories/document_category_factory.rb
new file mode 100644
index 0000000000..8cda71b2a8
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/factories/document_category_factory.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :document_category do
+ project
+ sequence(:name) { |n| "I am Category No. #{n}" }
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/factories/document_factory.rb b/vendored-plugins/openproject-documents/spec/factories/document_factory.rb
new file mode 100644
index 0000000000..32e4a79109
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/factories/document_factory.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+FactoryGirl.define do
+ factory :document do
+ project
+ category factory: :document_category
+ sequence(:description) { |n| "I am a document's description No. #{n}" }
+ sequence(:title) { |n| "I am the document No. #{n}" }
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/lib/acts_as_journalized/journaled_spec.rb b/vendored-plugins/openproject-documents/spec/lib/acts_as_journalized/journaled_spec.rb
new file mode 100644
index 0000000000..6a2b653c3a
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/lib/acts_as_journalized/journaled_spec.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe "Journalized Objects" do
+ before(:each) do
+ @type ||= FactoryGirl.create(:type_feature)
+ @project ||= FactoryGirl.create(:project_with_types)
+ @current = FactoryGirl.create(:user, login: "user1", mail: "user1@users.com")
+ allow(User).to receive(:current).and_return @current
+ end
+
+ it 'should work with documents' do
+ @document ||= FactoryGirl.create(:document)
+
+ initial_journal = @document.journals.first
+ recreated_journal = @document.recreate_initial_journal!
+
+ expect(initial_journal).to be_identical(recreated_journal)
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/lib/redmine/access_control_spec.rb b/vendored-plugins/openproject-documents/spec/lib/redmine/access_control_spec.rb
new file mode 100644
index 0000000000..e2c2939a37
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/lib/redmine/access_control_spec.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Redmine::AccessControl do
+
+ describe 'manage documents permission' do
+ it 'should be part of the documents project module' do
+ permission = Redmine::AccessControl.permission(:manage_documents)
+
+ expect(permission.project_module).to eql(:documents)
+ end
+ end
+
+ describe 'view documents permission' do
+ it 'should be part of the documents project module' do
+ permission = Redmine::AccessControl.permission(:view_documents)
+
+ expect(permission.project_module).to eql(:documents)
+ end
+ end
+
+end
diff --git a/vendored-plugins/openproject-documents/spec/mailers/documents_mailer_spec.rb b/vendored-plugins/openproject-documents/spec/mailers/documents_mailer_spec.rb
new file mode 100644
index 0000000000..1de6d9d1f8
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/mailers/documents_mailer_spec.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe DocumentsMailer do
+
+ let(:user) {
+ FactoryGirl.create(:user, firstname: 'Test', lastname: "User", mail: 'test@test.com')
+ }
+ let(:project) { FactoryGirl.create(:project, name: "TestProject") }
+ let(:document) {
+ FactoryGirl.create(:document, project: project, description: "Test Description", title: "Test Title" )
+ }
+ let(:mail) { DocumentsMailer.document_added(user, document) }
+
+ describe "document added-mail" do
+ it "renders the subject" do
+ expect(mail.subject).to eql '[TestProject] New document: Test Title'
+ end
+
+ it "should render the receivers mail" do
+ expect(mail.to.count).to eql 1
+ expect(mail.to.first).to eql user.mail
+ end
+
+ it "should render the document-info into the body" do
+ expect(mail.body.encoded).to match(document.description)
+ expect(mail.body.encoded).to match(document.title)
+ end
+
+ end
+
+
+
+
+end
diff --git a/vendored-plugins/openproject-documents/spec/models/document_category_spec.rb b/vendored-plugins/openproject-documents/spec/models/document_category_spec.rb
new file mode 100644
index 0000000000..12eca755ec
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/models/document_category_spec.rb
@@ -0,0 +1,68 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+require File.dirname(__FILE__) + '/../spec_helper'
+
+
+describe DocumentCategory do
+
+ let(:project) {FactoryGirl.create(:project)}
+
+ it "should be an enumeration" do
+ expect(DocumentCategory.ancestors).to include Enumeration
+ end
+
+ it "should order documents by the category they are created with" do
+ uncategorized = FactoryGirl.create :document_category, name: "Uncategorized", project: project
+ user_documentation = FactoryGirl.create :document_category, name: "User documentation"
+
+ FactoryGirl.create_list :document, 2, category: uncategorized, project: project
+
+ expect(DocumentCategory.find_by_name(uncategorized.name).objects_count).to eql 2
+ expect(DocumentCategory.find_by_name(user_documentation.name).objects_count).to eql 0
+
+ end
+
+ it "should file the categorizations under the option name :enumeration_doc_categories" do
+ expect(DocumentCategory.new.option_name).to eql :enumeration_doc_categories
+ end
+
+ it "should only allow one category to be the default-category" do
+ old_default = FactoryGirl.create :document_category, name: "old default", project: project, is_default: true
+
+ expect{
+ FactoryGirl.create :document_category, name: "new default", project: project, is_default: true
+ old_default.reload
+ }.to change{old_default.is_default?}.from(true).to(false)
+
+
+ end
+
+end
diff --git a/vendored-plugins/openproject-documents/spec/models/document_observer_spec.rb b/vendored-plugins/openproject-documents/spec/models/document_observer_spec.rb
new file mode 100644
index 0000000000..ea833e45b6
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/models/document_observer_spec.rb
@@ -0,0 +1,60 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe DocumentObserver do
+ let(:user) { FactoryGirl.create(:user, firstname: 'Test', lastname: "User", mail: 'test@test.com') }
+ let(:project) { FactoryGirl.create(:project, name: "TestProject")}
+
+ let(:mail) do
+ mock = Object.new
+ allow(mock).to receive(:deliver_now)
+ mock
+ end
+
+ it "is triggered, when a document has been created" do
+ document = FactoryGirl.build(:document)
+ #observers are singletons, so any_instance exactly leaves out the singleton
+ expect(DocumentObserver.instance).to receive(:after_create)
+ document.save!
+ end
+
+ it "calls the DocumentsMailer, when a new document has been added" do
+ document = FactoryGirl.build(:document)
+ # make sure, that we have actually someone to notify
+ allow(document).to receive(:recipients).and_return([user])
+ # ... and notifies are actually sent out
+ Setting.notified_events = Setting.notified_events << 'document_added'
+ expect(DocumentsMailer).to receive(:document_added).and_return(mail)
+
+ document.save
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/models/document_spec.rb b/vendored-plugins/openproject-documents/spec/models/document_spec.rb
new file mode 100644
index 0000000000..93c0434087
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/models/document_spec.rb
@@ -0,0 +1,113 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+require File.dirname(__FILE__) + '/../spec_helper'
+
+
+describe Document do
+
+ let(:documentation_category) { FactoryGirl.create :document_category, name: 'User documentation'}
+ let(:project) { FactoryGirl.create :project}
+ let(:user) { FactoryGirl.create(:user)}
+ let(:admin) { FactoryGirl.create(:admin)}
+
+ context "validation" do
+
+ it { is_expected.to validate_presence_of :project}
+ it { is_expected.to validate_presence_of :title}
+ it { is_expected.to validate_presence_of :category}
+
+ end
+
+ describe "create with a valid document" do
+
+ let(:valid_document) {Document.new(title: "Test", project: project, category: documentation_category)}
+
+ it "should add a document" do
+ expect{
+ valid_document.save
+ }.to change{Document.count}.by 1
+ end
+
+ it "should send out email-notifications" do
+ allow(valid_document).to receive(:recipients).and_return([user])
+ Setting.notified_events = Setting.notified_events << 'document_added'
+
+ expect{
+ valid_document.save
+ }.to change{ActionMailer::Base.deliveries.size}.by 1
+
+ end
+
+ it "should send notifications to the recipients of the project" do
+ allow(project).to receive(:notified_users).and_return([admin])
+ document = FactoryGirl.create(:document, project: project)
+
+ expect(document.recipients).not_to be_empty
+ expect(document.recipients.count).to eql 1
+ expect(document.recipients.map(&:mail)).to include admin.mail
+ end
+
+ it "should set a default-category, if none is given" do
+ default_category = FactoryGirl.create :document_category, name: 'Technical documentation', is_default: true
+ document = Document.new(project: project, title: "New Document")
+ expect(document.category).to eql default_category
+ expect{
+ document.save
+ }.to change{Document.count}.by 1
+ end
+
+ it "with attachments should change the updated_on-date on the document to the attachment's date" do
+ 3.times do
+ FactoryGirl.create(:attachment, container: valid_document)
+ end
+
+ valid_document.reload
+ expect(valid_document.attachments.size).to eql 3
+ expect(valid_document.attachments.map(&:created_on).max).to eql valid_document.updated_on
+ end
+
+ it "without attachments, the updated-on-date is taken from the document's date" do
+ document = FactoryGirl.create(:document, project: project)
+ expect(document.attachments).to be_empty
+ expect(document.created_on).to eql document.updated_on
+ end
+ end
+
+ describe "acts as event" do
+ let(:now) { Time.zone.now }
+ let(:document) {
+ FactoryGirl.build(:document,
+ created_on: now)
+ }
+
+ it { expect(document.event_datetime).to eq(now) }
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/routing/documents_routing_spec.rb b/vendored-plugins/openproject-documents/spec/routing/documents_routing_spec.rb
new file mode 100644
index 0000000000..3042923968
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/routing/documents_routing_spec.rb
@@ -0,0 +1,78 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe DocumentsController do
+ describe "routing" do
+ it {
+ expect(get('/projects/567/documents')).to route_to(controller: 'documents',
+ action: 'index',
+ project_id: '567' )
+ }
+
+ it {
+ expect(get('/projects/567/documents/new')).to route_to(controller: 'documents',
+ action: 'new',
+ project_id: '567' )
+ }
+
+ it {
+ expect(get('/documents/22')).to route_to(controller: 'documents',
+ action: 'show',
+ id: '22')
+ }
+
+ it {
+ expect(get('/documents/22/edit')).to route_to(controller: 'documents',
+ action: 'edit',
+ id: '22')
+ }
+
+ it {
+ expect(post('/projects/567/documents')).to route_to(controller: 'documents',
+ action: 'create',
+ project_id: '567')
+ }
+
+ it {
+ expect(put('/documents/567')).to route_to(controller: 'documents',
+ action: 'update',
+ id: '567')
+ }
+
+ it {
+ expect(delete('/documents/567')).to route_to(controller: 'documents',
+ action: 'destroy',
+ id: '567')
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-documents/spec/spec_helper.rb b/vendored-plugins/openproject-documents/spec/spec_helper.rb
new file mode 100644
index 0000000000..17f1e43851
--- /dev/null
+++ b/vendored-plugins/openproject-documents/spec/spec_helper.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject Documents Plugin
+#
+# Former OpenProject Core functionality extracted into a plugin.
+#
+# Copyright (C) 2009-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# -- load spec_helper from OpenProject core
+require "spec_helper"
diff --git a/vendored-plugins/openproject-github_integration/README.md b/vendored-plugins/openproject-github_integration/README.md
new file mode 100644
index 0000000000..8ab50df26f
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/README.md
@@ -0,0 +1,92 @@
+# OpenProject Github Integration Plugin
+
+`openproject-github_integration` is an OpenProject plugin, which aims to integrate GitHub code repositories and a pull request workflow with OpenProject.
+
+
+![GitHub Integration Screenshot](doc/screenshot.png?raw=true)
+
+Currently we support the following workflow. When you create a pull request and paste a work package URL into its description, the plugin will add a comment to the work package when the pull request is opened, closed, merged or reopened.
+
+If you forget to add the work package URL when creating the pull request, you can edit its description and add the URL, but this doesn't automatically add a comment to the work package (GitHub unfortunately doesn't notify us for this event). To nevertheless add a link between the pull request and the work package, you can add the work package URL in a pull request comment. The plugin then adds a comment to the work package.
+
+We plan to integrate better with GitHub (e.g. show GitHub repository content within OpenProject, comment/merge pull requests from within OpenProject etc.).
+To make that happen we happily integrate your pull requests :)
+
+## Requirements
+
+* Same OpenProject version as this plugin.
+* The [`openproject-webhooks`](https://github.com/finnlabs/openproject-webhooks) plugin
+* Repository management rights on the GitHub repositories you want to integrate
+
+## Installation
+
+This is an OpenProject plugin, thus we follow the usual OpenProject plugin installation mechanism.
+Because this plugin depends on the [`openproject-webhooks`](https://github.com/finnlabs/openproject-webhooks) plugin, we also install that plugin.
+
+### Plugin Installation
+
+Edit the `Gemfile.plugins` file in your openproject-installation directory to contain the following lines:
+
+```ruby
+gem "openproject-webhooks", :git => 'https://github.com/finnlabs/openproject-webhooks.git', :branch => 'stable'
+gem "openproject-github_integration", :git => 'https://github.com/finnlabs/openproject-github_integration.git', :branch => 'stable'
+```
+
+Then update your bundle with:
+
+ bundle install
+
+and restart the OpenProject server.
+
+### OpenProject configuration
+
+To enable GitHub integration we need an OpenProject API key of a user with sufficient rights on the projects which shall be synchronized.
+Any user will work, but we recommend to create a special 'GitHub' user in your OpenProject installation for that task.
+
+**Note:** Double check that the user whose API key you use has sufficient rights on the projects which shall be synced with GitHub. You can e.g. create a 'GitHub' role with 'Add notes' (Work package tracking) permission, assign the user to this role and add the user in this role to all Projects where you want the user to comment on work packages.
+
+### GitHub configuration
+
+Visit the settings page of the GitHub repository you want to integrate.
+Go to the "Webhooks & Services" page.
+
+Within the "Webhooks" section you can create a new webhook with the "Add webhook" button in the top-right corner.
+
+The **Payload URL** is `/webhooks/github?key=`.
+
+For **Payload version** select `application/vnd.github.v3+json` (not `...+form`!). If you see GitHub reporting a 403 error for the ping request later, make sure to select the correct one here.
+
+Then select the events which GitHub will send to your OpenProject installation.
+We currently only need `Pull Request` and `Issue Comment`, but its also ok to select the *Send me everything* option.
+
+## Contact
+
+OpenProject is supported by its community members, both companies and individuals.
+
+Please find ways to contact us on the OpenProject [support page](https://www.openproject.org/help).
+
+## Contributing
+
+This OpenProject plugin is an open source project and we encourage you to help us out. We'd be happy if you do one of these things:
+
+* Create a new [work package in the GitHub Integration plugin project on openproject.org](https://community.openproject.org/projects/github-integration) if you find a bug or need a feature
+* Help out other people on our [forums](https://community.openproject.org/projects/openproject/boards)
+* Contribute code via GitHub Pull Requests, see our [contribution page](https://www.openproject.org/open-source/code-contributions/) for more information
+
+## Community
+
+OpenProject is driven by an active group of open source enthusiasts: software engineers, project managers, creatives, and consultants. OpenProject is supported by companies as well as individuals. We share the vision to build great open source project collaboration software.
+The [OpenProject Foundation (OPF)](https://www.openproject.org/open-source/) will give official guidance to the project and the community and oversees contributions and decisions.
+
+## Repository
+
+This repository contains two main branches:
+
+* `dev`: The main development branch. We try to keep it stable in the sense of all tests are passing, but we don't recommend it for production systems.
+* `stable`: Contains the latest stable release that we recommend for production use. Use this if you always want the latest version of this plugin.
+
+## License
+
+Copyright (C) 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See [doc/COPYRIGHT.md](doc/COPYRIGHT.md) for details.
diff --git a/vendored-plugins/openproject-github_integration/config/locales/de.yml b/vendored-plugins/openproject-github_integration/config/locales/de.yml
new file mode 100644
index 0000000000..cf59266183
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/config/locales/de.yml
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+de:
+ github_integration:
+ pull_request_opened_comment: >
+ **PR Geöffnet:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} in "%{repository}":%{repository_url}
+ wurde von "%{github_user}":%{github_user_url} geöffnet.
+ pull_request_closed_comment: >
+ **PR Geschlossen:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} in "%{repository}":%{repository_url}
+ wurde von "%{github_user}":%{github_user_url} geschlossen.
+ pull_request_merged_comment: >
+ **PR Merged:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} in "%{repository}":%{repository_url}
+ wurde von "%{github_user}":%{github_user_url} gemerged.
+ pull_request_referenced_comment: >
+ **Referenziert in PR:** ""%{github_user}":%{github_user_url} hat dieses Arbeitspaket in
+ Pull Request %{pr_number} “%{pr_title}”":%{pr_url} in "%{repository}":%{repository_url}
+ referenziert.
diff --git a/vendored-plugins/openproject-github_integration/config/locales/en.yml b/vendored-plugins/openproject-github_integration/config/locales/en.yml
new file mode 100644
index 0000000000..4ca68f2fa6
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/config/locales/en.yml
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+en:
+ github_integration:
+ pull_request_opened_comment: >
+ **PR Opened:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} for "%{repository}":%{repository_url}
+ has been opened by "%{github_user}":%{github_user_url}.
+ pull_request_closed_comment: >
+ **PR Closed:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} for "%{repository}":%{repository_url}
+ has been closed by "%{github_user}":%{github_user_url}.
+ pull_request_merged_comment: >
+ **PR Merged:** "Pull request %{pr_number} “%{pr_title}”":%{pr_url} for "%{repository}":%{repository_url}
+ has been merged by "%{github_user}":%{github_user_url}.
+ pull_request_referenced_comment: >
+ **Referenced in PR:** "%{github_user}":%{github_user_url} referenced this work package in
+ "Pull request %{pr_number} “%{pr_title}”":%{pr_url} on "%{repository}":%{repository_url}.
diff --git a/vendored-plugins/openproject-github_integration/doc/CHANGELOG.md b/vendored-plugins/openproject-github_integration/doc/CHANGELOG.md
new file mode 100644
index 0000000000..b46fda0d29
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/doc/CHANGELOG.md
@@ -0,0 +1,18 @@
+
+
+# Changelog
+
+* `#4053` Create plugin
diff --git a/vendored-plugins/openproject-github_integration/doc/COPYRIGHT.md b/vendored-plugins/openproject-github_integration/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..3e6aac464e
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/doc/COPYRIGHT.md
@@ -0,0 +1,16 @@
+OpenProject is a project management system.
+
+Copyright (C) 2013 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-github_integration/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-github_integration/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..01c04a91d2
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/doc/COPYRIGHT_short.md
@@ -0,0 +1,11 @@
+OpenProject is a project management system.
+Copyright (C) 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
diff --git a/vendored-plugins/openproject-github_integration/doc/GPL.txt b/vendored-plugins/openproject-github_integration/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-github_integration/doc/screenshot.png b/vendored-plugins/openproject-github_integration/doc/screenshot.png
new file mode 100644
index 0000000000..eedeeaad24
Binary files /dev/null and b/vendored-plugins/openproject-github_integration/doc/screenshot.png differ
diff --git a/vendored-plugins/openproject-github_integration/lib/open_project/github_integration.rb b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration.rb
new file mode 100644
index 0000000000..ec5bb02e2a
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration.rb
@@ -0,0 +1,19 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module GithubIntegration
+ require "open_project/github_integration/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/engine.rb b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/engine.rb
new file mode 100644
index 0000000000..48b3c84c67
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/engine.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/plugins'
+# require 'open_project/notifications'
+
+module OpenProject::GithubIntegration
+ class Engine < ::Rails::Engine
+ engine_name :openproject_github_integration
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-github_integration',
+ :author_url => 'http://finn.de',
+ :requires_openproject => '>= 3.1.0pre1'
+
+
+ initializer 'github.register_hook' do
+ ::OpenProject::Webhooks.register_hook 'github' do |hook, environment, params, user|
+ HookHandler.new.process(hook, environment, params, user)
+ end
+ end
+
+ initializer 'github.subscribe_to_notifications' do
+ ::OpenProject::Notifications.subscribe('github.pull_request',
+ &NotificationHandlers.method(:pull_request))
+ ::OpenProject::Notifications.subscribe('github.issue_comment',
+ &NotificationHandlers.method(:issue_comment))
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/hook_handler.rb b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/hook_handler.rb
new file mode 100644
index 0000000000..19786b6c68
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/hook_handler.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::GithubIntegration
+ class HookHandler
+ # List of the github events we can handle.
+ KNOWN_EVENTS = %w{ ping pull_request issue_comment }
+
+ # A github webhook happened.
+ # We need to check validity of the data and send a Notification
+ # which we process in our NotificationHandler.
+ def process(hook, environment, params, user)
+ event_type = environment['HTTP_X_GITHUB_EVENT']
+ event_delivery = environment['HTTP_X_GITHUB_DELIVERY']
+
+ Rails.logger.debug "Received github webhook #{event_type} (#{event_delivery})"
+
+ return 404 unless KNOWN_EVENTS.include?(event_type) && event_delivery
+ return 403 unless user.present?
+
+ payload = Hash.new
+ payload.merge! params.require('webhook')
+ payload.merge! 'user_id' => user.id,
+ 'github_event' => event_type,
+ 'github_delivery' => event_delivery
+
+ OpenProject::Notifications.send(event_name(event_type), payload)
+
+ return 200
+ end
+
+ private def event_name(github_event_name)
+ "github.#{github_event_name}"
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/notification_handlers.rb b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/notification_handlers.rb
new file mode 100644
index 0000000000..3797f5fc2d
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/notification_handlers.rb
@@ -0,0 +1,175 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::GithubIntegration
+
+ ##
+ # Handles github-related notifications.
+ module NotificationHandlers
+
+ ##
+ # Handles a pull_request webhook notification.
+ # The payload looks similar to this:
+ # { user_id: ,
+ # github_event: 'pull_request',
+ # github_delivery: ,
+ # Have a look at the github documentation about the next keys:
+ # http://developer.github.com/v3/activity/events/types/#pullrequestevent
+ # action: 'opened' | 'closed' | 'synchronize' | 'reopened',
+ # number: ,
+ # pull_request:
+ # We observed the following keys to appear. However they are not documented by github
+ # sender: (might not appear on closed,
+ # synchronized, or reopened - we habven't checked)
+ # repository:
+ # }
+ def self.pull_request(payload)
+ # Don't add comments on new pushes to the pull request => ignore synchronize.
+ # Don't add comments about assignments and labels either.
+ ignored_actions = %w[synchronize assigned unassigned labeled unlabeled]
+ return if ignored_actions.include? payload['action']
+ comment_on_referenced_work_packages payload['pull_request']['body'], payload
+ end
+
+ ##
+ # Handles an issue_comment webhook notification.
+ # The payload looks similar to this:
+ # { user_id: ,
+ # github_event: 'issue_comment',
+ # github_delivery: ,
+ # Have a look at the github documentation about the next keys:
+ # http://developer.github.com/v3/activity/events/types/#pullrequestevent
+ # action: 'created',
+ # issue:
+ # comment:
+ # We observed the following keys to appear. However they are not documented by github
+ # sender: (might not appear on closed,
+ # synchronized, or reopened - we habven't checked)
+ # repository:
+ # }
+ def self.issue_comment(payload)
+ # if the comment is not associated with a PR, ignore it
+ return unless payload['issue']['pull_request']['html_url']
+ comment_on_referenced_work_packages payload['comment']['body'], payload
+ end
+
+ ##
+ # Parses the text for links to WorkPackages and adds a comment
+ # to those WorkPackages depending on the payload.
+ def self.comment_on_referenced_work_packages(text, payload)
+ user = User.find_by_id(payload['user_id'])
+ wp_ids = extract_work_package_ids(text)
+ wps = find_visible_work_packages(wp_ids, user)
+
+ # FIXME check user is allowed to update work packages
+ # TODO mergeable
+
+ wps.each do |wp|
+ wp.update_by!(user, :notes => notes_for_payload(payload))
+ end
+ end
+
+ ##
+ # Parses the given source string and returns a list of work_package ids
+ # which it finds.
+ # WorkPackages are identified by their URL.
+ # Params:
+ # source: string
+ # Returns:
+ # Array
+ def self.extract_work_package_ids(source)
+ # matches the following things (given that `Setting.host_name` equals 'www.openproject.org')
+ # - http://www.openproject.org/wp/1234
+ # - https://www.openproject.org/wp/1234
+ # - http://www.openproject.org/work_packages/1234
+ # - https://www.openproject.org/subdirectory/work_packages/1234
+ wp_regex = /http(?:s?):\/\/#{Regexp.escape(Setting.host_name)}\/(?:\S+?\/)*(?:work_packages|wp)\/([0-9]+)/
+
+ source.scan(wp_regex).flatten.map {|s| s.to_i }.uniq
+ end
+
+ ##
+ # Given a list of work package ids this methods returns all work packages that match those ids
+ # and are visible by the given user.
+ # Params:
+ # - Array: An list of WorkPackage ids
+ # - User: The user who may (or may not) see those WorkPackages
+ # Returns:
+ # - Array
+ def self.find_visible_work_packages(ids, user)
+ ids.collect do |id|
+ WorkPackage.includes(:project).find_by_id(id)
+ end.select do |wp|
+ wp.present? && user.allowed_to?(:add_work_package_notes, wp.project)
+ end
+ end
+
+ ##
+ # Find a matching translation for the action specified in the payload.
+ def self.notes_for_payload(payload)
+ case payload['github_event']
+ when 'pull_request'
+ notes_for_pull_request_payload(payload)
+ when 'issue_comment'
+ notes_for_issue_comment_payload(payload)
+ else
+ raise "GitHub event not supported: #{payload['github_event']}" +
+ " (#{payload['github_delivery']})"
+ end
+ end
+
+ def self.notes_for_pull_request_payload(payload)
+ key = {
+ 'opened' => 'opened',
+ 'reopened' => 'opened',
+ 'closed' => 'closed',
+ 'referenced' => 'referenced',
+ # We ignore synchrize actions for now. See pull_request method.
+ 'synchronize' => nil
+ }[payload['action']]
+
+ # a closed pull request which has been merged
+ # deserves a different label :)
+ key = 'merged' if key == 'closed' && payload['pull_request']['merged']
+
+ raise "Github action #{payload['action']} " +
+ "for event #{payload['github_event']} not supported." unless key
+
+ I18n.t("github_integration.pull_request_#{key}_comment",
+ :pr_number => payload['number'],
+ :pr_title => payload['pull_request']['title'],
+ :pr_url => payload['pull_request']['html_url'],
+ :repository => payload['pull_request']['base']['repo']['full_name'],
+ :repository_url => payload['pull_request']['base']['repo']['html_url'],
+ :github_user => payload['sender']['login'],
+ :github_user_url => payload['sender']['html_url'])
+ end
+
+ def self.notes_for_issue_comment_payload(payload)
+ unless payload['action'] == 'created'
+ raise "Github action #{payload['action']} " +
+ "for event #{payload['github_event']} not supported."
+ end
+
+ I18n.t("github_integration.pull_request_referenced_comment",
+ :pr_number => payload['issue']['number'],
+ :pr_title => payload['issue']['title'],
+ :pr_url => payload['comment']['html_url'],
+ :repository => payload['repository']['full_name'],
+ :repository_url => payload['repository']['html_url'],
+ :github_user => payload['comment']['user']['login'],
+ :github_user_url => payload['comment']['user']['html_url'])
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/version.rb b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/version.rb
new file mode 100644
index 0000000000..602dc50ddb
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/open_project/github_integration/version.rb
@@ -0,0 +1,19 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module GithubIntegration
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/lib/openproject-github_integration.rb b/vendored-plugins/openproject-github_integration/lib/openproject-github_integration.rb
new file mode 100644
index 0000000000..4f8eb10f6e
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/lib/openproject-github_integration.rb
@@ -0,0 +1,15 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/github_integration'
diff --git a/vendored-plugins/openproject-github_integration/openproject-github_integration.gemspec b/vendored-plugins/openproject-github_integration/openproject-github_integration.gemspec
new file mode 100644
index 0000000000..b502fd0f2f
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/openproject-github_integration.gemspec
@@ -0,0 +1,21 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/github_integration/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-github_integration"
+ s.version = OpenProject::GithubIntegration::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/github-integration"
+ s.summary = 'OpenProject Github Integration'
+ s.description = 'Integrates OpenProject and Github for a better workflow'
+ s.license = 'GPLv3'
+
+ s.files = Dir["{app,config,db,doc,lib}/**/*"] + %w(README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_dependency "openproject-webhooks", "~> 5.0.1"
+end
diff --git a/vendored-plugins/openproject-github_integration/spec/lib/github_integration_spec.rb b/vendored-plugins/openproject-github_integration/spec/lib/github_integration_spec.rb
new file mode 100644
index 0000000000..9b453aef37
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/spec/lib/github_integration_spec.rb
@@ -0,0 +1,228 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+describe OpenProject::GithubIntegration do
+ before do
+ allow(Setting).to receive(:host_name).and_return('example.net')
+ end
+
+ describe 'with sane set-up' do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:role) { FactoryGirl.create(:role,
+ permissions: [:add_work_package_notes]) }
+ let(:statuses) { (1..5).map{ |i| FactoryGirl.create(:status)}}
+ let(:priority) { FactoryGirl.create :priority, is_default: true }
+ let(:status) { statuses[0] }
+ let(:project) do
+ FactoryGirl.create(:project).tap do |p|
+ p.add_member(user, role).save
+ end
+ end
+ let(:project_without_permission) { FactoryGirl.create(:project) }
+ let(:wp1) do
+ FactoryGirl.create :work_package, project: project
+ end
+ let(:wp2) do
+ FactoryGirl.create :work_package, project: project
+ end
+ let(:wp3) do
+ FactoryGirl.create :work_package,
+ project: project_without_permission
+ end
+ let(:wp4) do
+ FactoryGirl.create :work_package,
+ project: project_without_permission
+ end
+ let(:wps) { [wp1, wp2, wp3, wp4] }
+
+ it "should handle the pull_request creation payload" do
+ params = ActionController::Parameters.new({
+ 'webhook' => {
+ 'action' => 'opened',
+ 'number' => '5',
+ 'pull_request' => {
+ 'title' => 'Bugfixes',
+ 'body' => "Fixes http://example.net/wp/#{wp1.id} and " +
+ "https://example.net/work_packages/#{wp2.id} and " +
+ "http://example.net/subdir/wp/#{wp3.id} and " +
+ "https://example.net/subdir/work_packages/#{wp4.id}.",
+ 'html_url' => 'http://pull.request',
+ 'base' => {
+ 'repo' => {
+ 'full_name' => 'full/name',
+ 'html_url' => 'http://pull.request'
+ }
+ }
+ },
+ 'sender' => {
+ 'login' => 'github_login',
+ 'html_url' => 'http://user.name'
+ },
+ 'repository' => {}
+ }
+ })
+
+ environment = {
+ 'HTTP_X_GITHUB_EVENT' => 'pull_request',
+ 'HTTP_X_GITHUB_DELIVERY' => 'test delivery'
+ }
+
+ journal_count = wps.map { |wp| wp.journals.count }
+ OpenProject::GithubIntegration::HookHandler.new.process('github', environment, params, user)
+
+ expect(wp1.journals.count).to equal(journal_count[0] + 1)
+ expect(wp2.journals.count).to equal(journal_count[1] + 1)
+ expect(wp3.journals.count).to equal(journal_count[2] + 0)
+ expect(wp4.journals.count).to equal(journal_count[3] + 0)
+
+ expect(wp1.journals.last.notes).to include('PR Opened')
+ end
+
+ it "should handle the pull_request close payload" do
+ params = ActionController::Parameters.new({
+ 'webhook' => {
+ 'action' => 'closed',
+ 'number' => '5',
+ 'pull_request' => {
+ 'title' => 'Bugfixes',
+ 'body' => "Fixes http://example.net/wp/#{wp1.id} and " +
+ "https://example.net/work_packages/#{wp2.id} and " +
+ "http://example.net/subdir/wp/#{wp3.id} and " +
+ "https://example.net/subdir/work_packages/#{wp4.id}.",
+ 'html_url' => 'http://pull.request',
+ 'base' => {
+ 'repo' => {
+ 'full_name' => 'full/name',
+ 'html_url' => 'http://pull.request'
+ }
+ }
+ },
+ 'sender' => {
+ 'login' => 'github_login',
+ 'html_url' => 'http://user.name'
+ },
+ 'repository' => {}
+ }
+ })
+
+ environment = {
+ 'HTTP_X_GITHUB_EVENT' => 'pull_request',
+ 'HTTP_X_GITHUB_DELIVERY' => 'test delivery'
+ }
+
+ journal_count = wps.map { |wp| wp.journals.count }
+ OpenProject::GithubIntegration::HookHandler.new.process('github', environment, params, user)
+
+ expect(wp1.journals.count).to equal(journal_count[0] + 1)
+ expect(wp2.journals.count).to equal(journal_count[1] + 1)
+ expect(wp3.journals.count).to equal(journal_count[2] + 0)
+ expect(wp4.journals.count).to equal(journal_count[3] + 0)
+
+ expect(wp1.journals.last.notes).to include('PR Closed')
+ end
+
+ it "should handle the pull_request merged payload" do
+ params = ActionController::Parameters.new({
+ 'webhook' => {
+ 'action' => 'closed',
+ 'number' => '5',
+ 'pull_request' => {
+ 'title' => 'Bugfixes',
+ 'body' => "Fixes http://example.net/wp/#{wp1.id} and " +
+ "https://example.net/work_packages/#{wp2.id} and " +
+ "http://example.net/subdir/wp/#{wp3.id} and " +
+ "https://example.net/subdir/work_packages/#{wp4.id}.",
+ 'html_url' => 'http://pull.request',
+ 'base' => {
+ 'repo' => {
+ 'full_name' => 'full/name',
+ 'html_url' => 'http://pull.request'
+ }
+ },
+ 'merged' => true
+ },
+ 'sender' => {
+ 'login' => 'github_login',
+ 'html_url' => 'http://user.name'
+ },
+ 'repository' => {}
+ }
+ })
+
+ environment = {
+ 'HTTP_X_GITHUB_EVENT' => 'pull_request',
+ 'HTTP_X_GITHUB_DELIVERY' => 'test delivery'
+ }
+
+ journal_count = wps.map { |wp| wp.journals.count }
+ OpenProject::GithubIntegration::HookHandler.new.process('github', environment, params, user)
+
+ expect(wp1.journals.count).to equal(journal_count[0] + 1)
+ expect(wp2.journals.count).to equal(journal_count[1] + 1)
+ expect(wp3.journals.count).to equal(journal_count[2] + 0)
+ expect(wp4.journals.count).to equal(journal_count[3] + 0)
+
+ expect(wp1.journals.last.notes).to include('PR Merged')
+ end
+
+ it "should handle the pull_request comment creation payload" do
+ params = ActionController::Parameters.new({
+ 'webhook' => {
+ 'action' => 'created',
+ 'issue' => {
+ 'title' => 'Bugfixes',
+ 'number' => '5',
+ 'pull_request' => {
+ 'html_url' => 'http://pull.request'
+ }
+ },
+ 'comment' => {
+ 'body' => "Fixes http://example.net/wp/#{wp1.id} and " +
+ "https://example.net/work_packages/#{wp2.id} and " +
+ "http://example.net/subdir/wp/#{wp3.id} and " +
+ "https://example.net/subdir/work_packages/#{wp4.id}.",
+ 'html_url' => 'http://comment.url',
+ 'user' => {
+ 'login' => 'github_login',
+ 'html_url' => 'http://user.name'
+ }
+ },
+ 'sender' => {
+ },
+ 'repository' => {
+ 'full_name' => 'full/name',
+ 'html_url' => 'http://pull.request'
+ }
+ }
+ })
+
+ environment = {
+ 'HTTP_X_GITHUB_EVENT' => 'issue_comment',
+ 'HTTP_X_GITHUB_DELIVERY' => 'test delivery'
+ }
+
+ journal_count = wps.map { |wp| wp.journals.count }
+ OpenProject::GithubIntegration::HookHandler.new.process('github', environment, params, user)
+
+ expect(wp1.journals.count).to equal(journal_count[0] + 1)
+ expect(wp2.journals.count).to equal(journal_count[1] + 1)
+ expect(wp3.journals.count).to equal(journal_count[2] + 0)
+ expect(wp4.journals.count).to equal(journal_count[3] + 0)
+
+ expect(wp1.journals.last.notes).to include('Referenced')
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/spec/lib/hook_handler_spec.rb b/vendored-plugins/openproject-github_integration/spec/lib/hook_handler_spec.rb
new file mode 100644
index 0000000000..76b72ac4c4
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/spec/lib/hook_handler_spec.rb
@@ -0,0 +1,84 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+describe OpenProject::GithubIntegration::HookHandler do
+ describe '#process' do
+ let(:handler) { OpenProject::GithubIntegration::HookHandler.new }
+ let(:hook) { 'fake hook' }
+ let(:params) { ActionController::Parameters.new({ 'webhook' => {'fake' => 'value'} }) }
+ let(:environment) { { 'HTTP_X_GITHUB_EVENT' => 'pull_request' ,
+ 'HTTP_X_GITHUB_DELIVERY' => 'veryuniqueid' } }
+ let(:user) do
+ user = double(User)
+ allow(user).to receive(:id).and_return(12)
+ user
+ end
+
+ context 'with an unsupported event' do
+ let(:environment) { { 'HTTP_X_GITHUB_EVENT' => 'X-unspupported' ,
+ 'HTTP_X_GITHUB_DELIVERY' => 'veryuniqueid2' } }
+
+ it 'should return 404' do
+ result = handler.process(hook, environment, params, user)
+ expect(result).to eq(404)
+ end
+ end
+
+ context 'with a supported event and without user' do
+ let(:user) { nil }
+
+ it 'should return 403' do
+ result = handler.process(hook, environment, params, user)
+ expect(result).to eq(403)
+ end
+ end
+
+ context 'with a supported event and a user' do
+ before do
+ allow(OpenProject::Notifications).to receive(:send)
+ end
+
+ it 'should send a notification with the correct contents' do
+ expect(OpenProject::Notifications).to receive(:send).with("github.pull_request", {
+ 'fake' => 'value',
+ 'user_id' => 12,
+ 'github_event' => 'pull_request',
+ 'github_delivery' => 'veryuniqueid'
+ })
+ handler.process(hook, environment, params, user)
+ end
+
+ it 'should return 200' do
+ result = handler.process(hook, environment, params, user)
+ expect(result).to eq(200)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/spec/lib/notification_handlers_spec.rb b/vendored-plugins/openproject-github_integration/spec/lib/notification_handlers_spec.rb
new file mode 100644
index 0000000000..4131d7a03b
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/spec/lib/notification_handlers_spec.rb
@@ -0,0 +1,154 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+describe OpenProject::GithubIntegration do
+ before do
+ allow(Setting).to receive(:host_name).and_return('example.net')
+ end
+
+ describe '.extract_work_package_ids' do
+ it 'should return an empty array for an empty source' do
+ result = OpenProject::GithubIntegration::NotificationHandlers.send(
+ :extract_work_package_ids, '')
+ expect(result).to eql([])
+ end
+
+ it 'should find a plain work package url' do
+ source = 'Blabla\nhttps://example.net/work_packages/234\n'
+ result = OpenProject::GithubIntegration::NotificationHandlers.send(
+ :extract_work_package_ids, source)
+ expect(result).to eql([234])
+ end
+
+ it 'should find a work package url in markdown link syntax' do
+ source = 'Blabla\n[WP 234](https://example.net/work_packages/234)\n'
+ result = OpenProject::GithubIntegration::NotificationHandlers.send(
+ :extract_work_package_ids, source)
+ expect(result).to eql([234])
+ end
+
+ it 'should find multiple work package urls' do
+ source = "I reference https://example.net/work_packages/434\n and Blabla\n[WP 234](https://example.net/wp/234)\n"
+ result = OpenProject::GithubIntegration::NotificationHandlers.send(
+ :extract_work_package_ids, source)
+ expect(result).to eql([434, 234])
+ end
+
+ it 'should find multiple occurences of a work package only once' do
+ source = "I reference https://example.net/work_packages/434\n and Blabla\n[WP 234](https://example.net/work_packages/434)\n"
+ result = OpenProject::GithubIntegration::NotificationHandlers.send(
+ :extract_work_package_ids, source)
+ expect(result).to eql([434])
+ end
+ end
+
+ describe '.find_visible_work_packages' do
+ let(:user) do
+ user = double('A User')
+ expect(user).to receive(:allowed_to?) { |permission, project|
+ expect(permission).to equal(:add_work_package_notes)
+ project == :project_with_permissions
+ }.at_least(:once)
+ user
+ end
+ let(:visible_wp) do
+ wp = double('Visible Work Package')
+ allow(wp).to receive(:project).and_return(:project_with_permissions)
+ wp
+ end
+ let(:invisible_wp) do
+ wp = double('Invisible Work Package')
+ allow(wp).to receive(:project).and_return(:project_without_permissions)
+ wp
+ end
+
+ before do
+ allow(WorkPackage).to receive(:includes).and_return(WorkPackage)
+ allow(WorkPackage).to receive(:find_by_id) {|id| wps[id]}
+ end
+
+ shared_examples_for 'GithubIntegration.find_visible_work_packages' do
+ subject { OpenProject::GithubIntegration::NotificationHandlers.send(
+ :find_visible_work_packages, ids, user) }
+ it { expect(subject).to eql(expected) }
+ end
+
+ describe 'should find an existing work package' do
+ let(:wps) { [visible_wp] }
+ let(:ids) { [0] }
+ let(:expected) { wps }
+
+ it_behaves_like 'GithubIntegration.find_visible_work_packages'
+ end
+
+ describe 'should not find a non-existing work package' do
+ let(:wps) { [invisible_wp] }
+ let(:ids) { [0] }
+ let(:expected) { [] }
+
+ it_behaves_like 'GithubIntegration.find_visible_work_packages'
+ end
+
+ describe 'should find multiple existing work packages' do
+ let(:wps) { [visible_wp, visible_wp] }
+ let(:ids) { [0, 1] }
+ let(:expected) { wps }
+
+ it_behaves_like 'GithubIntegration.find_visible_work_packages'
+ end
+
+ describe 'should not find work package which the user shall not see' do
+ let(:wps) { [visible_wp, invisible_wp, visible_wp, invisible_wp] }
+ let(:ids) { [0, 1, 2, 3] }
+ let(:expected) { [visible_wp, visible_wp] }
+
+ it_behaves_like 'GithubIntegration.find_visible_work_packages'
+ end
+ end
+
+ describe '.issue_comment' do
+ context 'for a non-pull request issue' do
+ let(:payload) do
+ { 'action' => 'created',
+ 'issue' => { 'pull_request' => { 'html_url' => nil } } }
+ end
+
+ before do
+ expect(OpenProject::GithubIntegration::NotificationHandlers).not_to receive(
+ :comment_on_referenced_work_packages)
+ end
+
+ it 'should do nothing' do
+ OpenProject::GithubIntegration::NotificationHandlers.issue_comment(payload)
+ end
+ end
+ end
+
+ describe '.pull_request' do
+ context 'with a synchronize action' do
+ let(:payload) { {'action' => 'synchronize'} }
+
+ before do
+ expect(OpenProject::GithubIntegration::NotificationHandlers).not_to receive(
+ :comment_on_referenced_work_packages)
+ end
+
+ it 'should do nothing' do
+ OpenProject::GithubIntegration::NotificationHandlers.pull_request(payload)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-github_integration/spec/spec_helper.rb b/vendored-plugins/openproject-github_integration/spec/spec_helper.rb
new file mode 100644
index 0000000000..be3099880b
--- /dev/null
+++ b/vendored-plugins/openproject-github_integration/spec/spec_helper.rb
@@ -0,0 +1,15 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'spec_helper'
diff --git a/vendored-plugins/openproject-global_roles/.rubocop.yml b/vendored-plugins/openproject-global_roles/.rubocop.yml
new file mode 100644
index 0000000000..d32efc64ee
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/.rubocop.yml
@@ -0,0 +1,1007 @@
+Style/AccessModifierIndentation:
+ Description: Check indentation of private/protected visibility modifiers.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
+ Enabled: true
+
+Style/AccessorMethodName:
+ Description: Check the naming of accessor methods for get_/set_.
+ Enabled: false
+
+Style/Alias:
+ Description: 'Use alias_method instead of alias.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
+ Enabled: true
+
+Style/AlignArray:
+ Description: >-
+ Align the elements of an array literal if they span more than
+ one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
+ Enabled: true
+
+Style/AlignHash:
+ Description: >-
+ Align the elements of a hash literal if they span more than
+ one line.
+ Enabled: true
+
+Style/AlignParameters:
+ Description: >-
+ Align the parameters of a method call if they span more
+ than one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
+ Enabled: false
+
+Style/AndOr:
+ Description: 'Use &&/|| instead of and/or.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
+ Enabled: false
+
+Style/ArrayJoin:
+ Description: 'Use Array#join instead of Array#*.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
+ Enabled: false
+
+Style/AsciiComments:
+ Description: 'Use only ascii symbols in comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
+ Enabled: false
+
+Style/AsciiIdentifiers:
+ Description: 'Use only ascii symbols in identifiers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
+ Enabled: true
+
+Style/Attr:
+ Description: 'Checks for uses of Module#attr.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
+ Enabled: false
+
+Style/BeginBlock:
+ Description: 'Avoid the use of BEGIN blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
+ Enabled: true
+
+Style/BarePercentLiterals:
+ Description: 'Checks if usage of %() or %Q() matches configuration.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
+ Enabled: false
+
+Style/BlockComments:
+ Description: 'Do not use block comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
+ Enabled: false
+
+Style/BlockDelimiters:
+ Description: >-
+ Avoid using {...} for multi-line blocks (multiline chaining is
+ always ugly).
+ Prefer {...} over do...end for single-line blocks.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/BlockEndNewline:
+ Description: 'Put end statement of multiline block on its own line.'
+ Enabled: true
+
+Style/BracesAroundHashParameters:
+ Description: 'Enforce braces style around hash parameters.'
+ Enabled: false
+
+Style/CaseEquality:
+ Description: 'Avoid explicit use of the case equality operator(===).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
+ Enabled: false
+
+Style/CaseIndentation:
+ Description: 'Indentation of when in a case/when/[else/]end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
+ Enabled: true
+
+Style/CharacterLiteral:
+ Description: 'Checks for uses of character literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
+ Enabled: true
+
+Style/ClassAndModuleCamelCase:
+ Description: 'Use CamelCase for classes and modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
+ Enabled: true
+
+Style/ClassAndModuleChildren:
+ Description: 'Checks style of children classes and modules.'
+ Enabled: false
+
+Style/ClassCheck:
+ Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
+ Enabled: false
+
+Style/ClassMethods:
+ Description: 'Use self when defining module/class methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-singletons'
+ Enabled: false
+
+Style/ClassVars:
+ Description: 'Avoid the use of class variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
+ Enabled: true
+
+Style/ColonMethodCall:
+ Description: 'Do not use :: for method call.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
+ Enabled: false
+
+Style/CommentAnnotation:
+ Description: >-
+ Checks formatting of special comments
+ (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
+ Enabled: false
+
+Style/CommentIndentation:
+ Description: 'Indentation of comments.'
+ Enabled: true
+
+Style/ConstantName:
+ Description: 'Constants should use SCREAMING_SNAKE_CASE.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
+ Enabled: true
+
+Style/DefWithParentheses:
+ Description: 'Use def with parentheses when there are arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/DeprecatedHashMethods:
+ Description: 'Checks for use of deprecated Hash methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
+ Enabled: false
+
+Style/Documentation:
+ Description: 'Document classes and non-namespace modules.'
+ Enabled: false
+
+Style/DotPosition:
+ Description: 'Checks the position of the dot in multi-line method calls.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
+ Enabled: false
+
+Style/DoubleNegation:
+ Description: 'Checks for uses of double negation (!!).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
+ Enabled: false
+
+Style/EachWithObject:
+ Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
+ Enabled: false
+
+Style/ElseAlignment:
+ Description: 'Align elses and elsifs correctly.'
+ Enabled: true
+
+Style/EmptyElse:
+ Description: 'Avoid empty else-clauses.'
+ Enabled: false
+
+Style/EmptyLineBetweenDefs:
+ Description: 'Use empty lines between defs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
+ Enabled: false
+
+Style/EmptyLines:
+ Description: "Don't use several empty lines in a row."
+ Enabled: false
+
+Style/EmptyLinesAroundAccessModifier:
+ Description: "Keep blank lines around access modifiers."
+ Enabled: false
+
+Style/EmptyLinesAroundBlockBody:
+ Description: "Keeps track of empty lines around block bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundClassBody:
+ Description: "Keeps track of empty lines around class bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundModuleBody:
+ Description: "Keeps track of empty lines around module bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundMethodBody:
+ Description: "Keeps track of empty lines around method bodies."
+ Enabled: false
+
+Style/EmptyLiteral:
+ Description: 'Prefer literals to Array.new/Hash.new/String.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
+ Enabled: false
+
+Style/EndBlock:
+ Description: 'Avoid the use of END blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
+ Enabled: false
+
+Style/EndOfLine:
+ Description: 'Use Unix-style line endings.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
+ Enabled: false
+
+Style/EvenOdd:
+ Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: false
+
+Style/FileName:
+ Description: 'Use snake_case for source file names.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
+ Enabled: false
+
+Style/FlipFlop:
+ Description: 'Checks for flip flops'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
+ Enabled: false
+
+Style/For:
+ Description: 'Checks use of for or each in multiline loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
+ Enabled: false
+
+Style/FormatString:
+ Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
+ Enabled: false
+
+Style/GlobalVars:
+ Description: 'Do not introduce global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
+ Enabled: false
+
+Style/GuardClause:
+ Description: 'Check for conditionals that can be replaced with guard clauses'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/HashSyntax:
+ Description: >-
+ Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
+ { :a => 1, :b => 2 }.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
+ Enabled: true
+
+Style/IfUnlessModifier:
+ Description: >-
+ Favor modifier if/unless usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
+ Enabled: false
+
+Style/IfWithSemicolon:
+ Description: 'Do not use if x; .... Use the ternary operator instead.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
+ Enabled: false
+
+Style/IndentationConsistency:
+ Description: 'Keep indentation straight.'
+ Enabled: true
+
+Style/IndentationWidth:
+ Description: 'Use 2 spaces for indentation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/IndentArray:
+ Description: >-
+ Checks the indentation of the first element in an array
+ literal.
+ Enabled: false
+
+Style/IndentHash:
+ Description: 'Checks the indentation of the first key in a hash literal.'
+ Enabled: false
+
+Style/InfiniteLoop:
+ Description: 'Use Kernel#loop for infinite loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
+ Enabled: false
+
+Style/Lambda:
+ Description: 'Use the new lambda literal syntax for single-line blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
+ Enabled: false
+
+Style/LambdaCall:
+ Description: 'Use lambda.call(...) instead of lambda.(...).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
+ Enabled: false
+
+Style/LeadingCommentSpace:
+ Description: 'Comments should start with a space.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
+ Enabled: false
+
+Style/LineEndConcatenation:
+ Description: >-
+ Use \ instead of + or << to concatenate two string literals at
+ line end.
+ Enabled: false
+
+Style/MethodCallParentheses:
+ Description: 'Do not use parentheses for method calls with no arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
+ Enabled: false
+
+Style/MethodDefParentheses:
+ Description: >-
+ Checks if the method definitions have or don't have
+ parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/MethodName:
+ Description: 'Use the configured style when naming methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/ModuleFunction:
+ Description: 'Checks for usage of `extend self` in modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
+ Enabled: false
+
+Style/MultilineBlockChain:
+ Description: 'Avoid multi-line chains of blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/MultilineBlockLayout:
+ Description: 'Ensures newlines after multiline block do statements.'
+ Enabled: true
+
+Style/MultilineIfThen:
+ Description: 'Do not use then for multi-line if/unless.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
+ Enabled: false
+
+Style/MultilineOperationIndentation:
+ Description: >-
+ Checks indentation of binary operations that span more than
+ one line.
+ Enabled: false
+
+Style/MultilineTernaryOperator:
+ Description: >-
+ Avoid multi-line ?: (the ternary operator);
+ use if/unless instead.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
+ Enabled: false
+
+Style/NegatedIf:
+ Description: >-
+ Favor unless over if for negative conditions
+ (or control flow or).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
+ Enabled: false
+
+Style/NegatedWhile:
+ Description: 'Favor until over while for negative conditions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
+ Enabled: false
+
+Style/NestedTernaryOperator:
+ Description: 'Use one expression per branch in a ternary operator.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
+ Enabled: true
+
+Style/Next:
+ Description: 'Use `next` to skip iteration instead of a condition at the end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/NilComparison:
+ Description: 'Prefer x.nil? to x == nil.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: true
+
+Style/NonNilCheck:
+ Description: 'Checks for redundant nil checks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
+ Enabled: true
+
+Style/Not:
+ Description: 'Use ! instead of not.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
+ Enabled: true
+
+Style/NumericLiterals:
+ Description: >-
+ Add underscores to large numeric literals to improve their
+ readability.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
+ Enabled: false
+
+Style/OneLineConditional:
+ Description: >-
+ Favor the ternary operator(?:) over
+ if/then/else/end constructs.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
+ Enabled: true
+
+Style/OpMethod:
+ Description: 'When defining binary operators, name the argument other.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
+ Enabled: false
+
+Style/ParenthesesAroundCondition:
+ Description: >-
+ Don't use parentheses around the condition of an
+ if/unless/while.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
+ Enabled: true
+
+Style/PercentLiteralDelimiters:
+ Description: 'Use `%`-literal delimiters consistently'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
+ Enabled: false
+
+Style/PercentQLiterals:
+ Description: 'Checks if uses of %Q/%q match the configured preference.'
+ Enabled: false
+
+Style/PerlBackrefs:
+ Description: 'Avoid Perl-style regex back references.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
+ Enabled: false
+
+Style/PredicateName:
+ Description: 'Check the names of predicate methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
+ Enabled: false
+
+Style/Proc:
+ Description: 'Use proc instead of Proc.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
+ Enabled: false
+
+Style/RaiseArgs:
+ Description: 'Checks the arguments passed to raise/fail.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
+ Enabled: false
+
+Style/RedundantBegin:
+ Description: "Don't use begin blocks when they are not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
+ Enabled: false
+
+Style/RedundantException:
+ Description: "Checks for an obsolete RuntimeException argument in raise/fail."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
+ Enabled: false
+
+Style/RedundantReturn:
+ Description: "Don't use return where it's not required."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
+ Enabled: true
+
+Style/RedundantSelf:
+ Description: "Don't use self where it's not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
+ Enabled: false
+
+Style/RegexpLiteral:
+ Description: >-
+ Use %r for regular expressions matching more than
+ `MaxSlashes` '/' characters.
+ Use %r only for regular expressions matching more than
+ `MaxSlashes` '/' character.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
+ Enabled: false
+
+Style/RescueModifier:
+ Description: 'Avoid using rescue in its modifier form.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
+ Enabled: false
+
+Style/SelfAssignment:
+ Description: >-
+ Checks for places where self-assignment shorthand should have
+ been used.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
+ Enabled: false
+
+Style/Semicolon:
+ Description: "Don't use semicolons to terminate expressions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
+ Enabled: false
+
+Style/SignalException:
+ Description: 'Checks for proper usage of fail and raise.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
+ Enabled: false
+
+Style/SingleLineBlockParams:
+ Description: 'Enforces the names of some block params.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
+ Enabled: false
+
+Style/SingleLineMethods:
+ Description: 'Avoid single-line methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
+ Enabled: false
+
+Style/SingleSpaceBeforeFirstArg:
+ Description: >-
+ Checks that exactly one space is used between a method name
+ and the first argument for method calls without parentheses.
+ Enabled: false
+
+Style/SpaceAfterColon:
+ Description: 'Use spaces after colons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterComma:
+ Description: 'Use spaces after commas.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterControlKeyword:
+ Description: 'Use spaces after if/elsif/unless/while/until/case/when.'
+ Enabled: false
+
+Style/SpaceAfterMethodName:
+ Description: >-
+ Do not put a space between a method name and the opening
+ parenthesis in a method definition.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: false
+
+Style/SpaceAfterNot:
+ Description: Tracks redundant space after the ! operator.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
+ Enabled: false
+
+Style/SpaceAfterSemicolon:
+ Description: 'Use spaces after semicolons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeBlockBraces:
+ Description: >-
+ Checks that the left block brace has or doesn't have space
+ before it.
+ Enabled: false
+
+Style/SpaceBeforeComma:
+ Description: 'No spaces before commas.'
+ Enabled: false
+
+Style/SpaceBeforeComment:
+ Description: >-
+ Checks for missing space between code and a comment on the
+ same line.
+ Enabled: false
+
+Style/SpaceBeforeSemicolon:
+ Description: 'No spaces before semicolons.'
+ Enabled: false
+
+Style/SpaceInsideBlockBraces:
+ Description: >-
+ Checks that block braces have or don't have surrounding space.
+ For blocks taking parameters, checks that the left brace has
+ or doesn't have trailing space.
+ Enabled: false
+
+Style/SpaceAroundEqualsInParameterDefault:
+ Description: >-
+ Checks that the equals signs in parameter default assignments
+ have or don't have surrounding space depending on
+ configuration.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
+ Enabled: false
+
+Style/SpaceAroundOperators:
+ Description: 'Use spaces around operators.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeModifierKeyword:
+ Description: 'Put a space before the modifier keyword.'
+ Enabled: false
+
+Style/SpaceInsideBrackets:
+ Description: 'No spaces after [ or before ].'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideHashLiteralBraces:
+ Description: "Use spaces inside hash literal braces - or don't."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: true
+
+Style/SpaceInsideParens:
+ Description: 'No spaces after ( or before ).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideRangeLiteral:
+ Description: 'No spaces inside range literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
+ Enabled: false
+
+Style/SpecialGlobalVars:
+ Description: 'Avoid Perl-style global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
+ Enabled: false
+
+Style/StringLiterals:
+ Description: 'Checks if uses of quotes match the configured preference.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
+ Enabled: false
+
+Style/StringLiteralsInInterpolation:
+ Description: >-
+ Checks if uses of quotes inside expressions in interpolated
+ strings match the configured preference.
+ Enabled: false
+
+Style/StructInheritance:
+ Enabled: false
+
+Style/SymbolProc:
+ Description: 'Use symbols as procs instead of blocks when possible.'
+ Enabled: false
+
+Style/Tab:
+ Description: 'No hard tabs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/TrailingBlankLines:
+ Description: 'Checks trailing blank lines and final newline.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
+ Enabled: true
+
+Style/TrailingComma:
+ Description: 'Checks for trailing comma in parameter lists and literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
+ Enabled: false
+
+Style/TrailingWhitespace:
+ Description: 'Avoid trailing whitespace.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
+ Enabled: false
+
+Style/TrivialAccessors:
+ Description: 'Prefer attr_* methods to trivial readers/writers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
+ Enabled: false
+
+Style/UnlessElse:
+ Description: >-
+ Do not use unless with else. Rewrite these with the positive
+ case first.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
+ Enabled: false
+
+Style/UnneededCapitalW:
+ Description: 'Checks for %W when interpolation is not needed.'
+ Enabled: false
+
+Style/UnneededPercentQ:
+ Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
+ Enabled: false
+
+Style/VariableInterpolation:
+ Description: >-
+ Don't interpolate global, instance and class variables
+ directly in strings.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
+ Enabled: false
+
+Style/VariableName:
+ Description: 'Use the configured style when naming variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/WhenThen:
+ Description: 'Use when x then ... for one-line cases.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
+ Enabled: false
+
+Style/WhileUntilDo:
+ Description: 'Checks for redundant do after while or until.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
+ Enabled: false
+
+Style/WhileUntilModifier:
+ Description: >-
+ Favor modifier while/until usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
+ Enabled: false
+
+Style/WordArray:
+ Description: 'Use %w or %W for arrays of words.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
+ Enabled: false
+
+#################### Metrics ################################
+
+Metrics/AbcSize:
+ Description: >-
+ A calculated magnitude based on number of assignments,
+ branches, and conditions.
+ Enabled: false
+
+Metrics/BlockNesting:
+ Description: 'Avoid excessive block nesting'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
+ Enabled: false
+
+Metrics/ClassLength:
+ Description: 'Avoid classes longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/ModuleLength:
+ Description: 'Avoid modules longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/CyclomaticComplexity:
+ Description: >-
+ A complexity metric that is strongly correlated to the number
+ of test cases needed to validate a method.
+ Enabled: false
+
+Metrics/LineLength:
+ Description: 'Limit lines to 80 characters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
+ Enabled: true
+ Max: 120
+
+Metrics/MethodLength:
+ Description: 'Avoid methods longer than 10 lines of code.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
+ Enabled: false
+
+Metrics/ParameterLists:
+ Description: 'Avoid parameter lists longer than three or four parameters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
+ Enabled: false
+
+Metrics/PerceivedComplexity:
+ Description: >-
+ A complexity metric geared towards measuring complexity for a
+ human reader.
+ Enabled: false
+
+#################### Lint ################################
+### Warnings
+
+Lint/AmbiguousOperator:
+ Description: >-
+ Checks for ambiguous operators in the first argument of a
+ method invocation without parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
+ Enabled: false
+
+Lint/AmbiguousRegexpLiteral:
+ Description: >-
+ Checks for ambiguous regexp literals in the first argument of
+ a method invocation without parenthesis.
+ Enabled: false
+
+Lint/AssignmentInCondition:
+ Description: "Don't use assignment in conditions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
+ Enabled: false
+
+Lint/BlockAlignment:
+ Description: 'Align block ends correctly.'
+ Enabled: false
+
+Lint/ConditionPosition:
+ Description: >-
+ Checks for condition placed in a confusing position relative to
+ the keyword.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
+ Enabled: false
+
+Lint/Debugger:
+ Description: 'Check for debugger calls.'
+ Enabled: true
+
+Lint/DefEndAlignment:
+ Description: 'Align ends corresponding to defs correctly.'
+ Enabled: false
+
+Lint/DeprecatedClassMethods:
+ Description: 'Check for deprecated class method calls.'
+ Enabled: false
+
+Lint/ElseLayout:
+ Description: 'Check for odd code arrangement in an else block.'
+ Enabled: false
+
+Lint/EmptyEnsure:
+ Description: 'Checks for empty ensure block.'
+ Enabled: false
+
+Lint/EmptyInterpolation:
+ Description: 'Checks for empty string interpolation.'
+ Enabled: false
+
+Lint/EndAlignment:
+ Description: 'Align ends correctly.'
+ Enabled: false
+
+Lint/EndInMethod:
+ Description: 'END blocks should not be placed inside method definitions.'
+ Enabled: false
+
+Lint/EnsureReturn:
+ Description: 'Do not use return in an ensure block.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
+ Enabled: false
+
+Lint/Eval:
+ Description: 'The use of eval represents a serious security risk.'
+ Enabled: false
+
+Lint/HandleExceptions:
+ Description: "Don't suppress exception."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
+ Enabled: false
+
+Lint/InvalidCharacterLiteral:
+ Description: >-
+ Checks for invalid character literals with a non-escaped
+ whitespace character.
+ Enabled: false
+
+Lint/LiteralInCondition:
+ Description: 'Checks of literals used in conditions.'
+ Enabled: false
+
+Lint/LiteralInInterpolation:
+ Description: 'Checks for literals used in interpolation.'
+ Enabled: false
+
+Lint/Loop:
+ Description: >-
+ Use Kernel#loop with break rather than begin/end/until or
+ begin/end/while for post-loop tests.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
+ Enabled: false
+
+Lint/NestedMethodDefinition:
+ Description: 'Do not use nested method definitions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods'
+ Enabled: false
+
+Lint/NonLocalExitFromIterator:
+ Description: 'Do not use return in iterator to cause non-local exit.'
+ Enabled: true
+
+Lint/ParenthesesAsGroupedExpression:
+ Description: >-
+ Checks for method calls with a space before the opening
+ parenthesis.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: true
+
+Lint/RequireParentheses:
+ Description: >-
+ Use parentheses in the method call to avoid confusion
+ about precedence.
+ Enabled: false
+
+Lint/RescueException:
+ Description: 'Avoid rescuing the Exception class.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
+ Enabled: true
+
+Lint/ShadowingOuterLocalVariable:
+ Description: >-
+ Do not use the same name as outer local variable
+ for block arguments or block local variables.
+ Enabled: false
+
+Lint/SpaceBeforeFirstArg:
+ Description: >-
+ Put a space between a method name and the first argument
+ in a method call without parentheses.
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Description: 'Checks for Object#to_s usage in string interpolation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
+ Enabled: true
+
+Lint/UnderscorePrefixedVariableName:
+ Description: 'Do not use prefix `_` for a variable that is used.'
+ Enabled: true
+
+Lint/UnusedBlockArgument:
+ Description: 'Checks for unused block arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnusedMethodArgument:
+ Description: 'Checks for unused method arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnreachableCode:
+ Description: 'Unreachable code.'
+ Enabled: true
+
+Lint/UselessAccessModifier:
+ Description: 'Checks for useless access modifiers.'
+ Enabled: false
+
+Lint/UselessAssignment:
+ Description: 'Checks for useless assignment to a local variable.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UselessComparison:
+ Description: 'Checks for comparison of something with itself.'
+ Enabled: false
+
+Lint/UselessElseWithoutRescue:
+ Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
+ Enabled: true
+
+Lint/UselessSetterCall:
+ Description: 'Checks for useless setter call to a local variable.'
+ Enabled: false
+
+Lint/Void:
+ Description: 'Possible use of operator/literal/variable in void context.'
+ Enabled: false
+
+##################### Rails ##################################
+
+Rails/ActionFilter:
+ Description: 'Enforces consistent use of action filter methods.'
+ Enabled: true
+
+Rails/DefaultScope:
+ Description: 'Checks if the argument passed to default_scope is a block.'
+ Enabled: false
+
+Rails/Delegate:
+ Description: 'Prefer delegate method for delegations.'
+ Enabled: false
+
+Rails/HasAndBelongsToMany:
+ Description: 'Prefer has_many :through to has_and_belongs_to_many.'
+ Enabled: false
+
+Rails/Output:
+ Description: 'Checks for calls to puts, print, etc.'
+ Enabled: true
+
+Rails/ReadWriteAttribute:
+ Description: >-
+ Checks for read_attribute(:attr) and
+ write_attribute(:attr, val).
+ Enabled: false
+
+Rails/ScopeArgs:
+ Description: 'Checks the arguments of ActiveRecord scopes.'
+ Enabled: false
+
+Rails/Validation:
+ Description: 'Use validates :attribute, hash of validations.'
+ Enabled: false
+
+AllCops:
+ RunRailsCops: true
+ Exclude:
+ - 'vendor/**/*'
+ - 'db/**/*'
+ - 'tmp/**/*'
+ - 'bin/**/*'
diff --git a/vendored-plugins/openproject-global_roles/.travis.yml b/vendored-plugins/openproject-global_roles/.travis.yml
new file mode 100644
index 0000000000..fba2a7a082
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/.travis.yml
@@ -0,0 +1,111 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Travis configuration based on the respective OpenProject core configuration.
+# Everything save for the matrix section and additional `before_install`
+# instructions is copied and pasted from the core.
+
+language: ruby
+
+rvm:
+ - 2.2.3
+
+sudo: false
+
+cache:
+ - bundler: true
+ - directories:
+ - frontend/node_modules
+ - frontend/bower_components
+
+bundler_args: --without development production
+
+branches:
+ only:
+ - master
+ - dev
+ - /^(stable|release)\/.*$/
+
+env:
+ global:
+ - CI=true
+ - RAILS_ENV=test
+ - COVERAGE=true
+
+ matrix:
+ - "TEST_SUITE=plugins:spec DB=mysql"
+ - "TEST_SUITE=plugins:cucumber DB=mysql"
+
+before_install:
+ # Custom plugin instructions follow.
+
+ # Move the plugin into a subfolder. The plugin-provided Gemfile.plugins
+ # must refer to this folder.
+ - mkdir -p plugins/this
+ - echo `ls -a | tail -n+3 | grep -v plugins` plugins/this/ | xargs mv
+
+ # Get OpenProject.
+ # Doing the fetch detour as you cannot clone into the current directory.
+ - git init
+ - git remote add openproject https://github.com/opf/openproject.git
+ - git fetch --depth=1 openproject
+ - git checkout openproject/dev
+
+ # End of custom plugin instructions.
+
+ - "echo `firefox -v`"
+ - "export DISPLAY=:99.0"
+ - "/sbin/start-stop-daemon --start -v --pidfile ./tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x16"
+ - "echo `xdpyinfo -display :99 | grep 'dimensions' | awk '{ print $2 }'`"
+ - travis_retry npm install
+
+ # We need phantomjs 2.0 to get tests passing
+ - mkdir travis-phantomjs
+ - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2
+ - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs
+ - export PATH=$PWD/travis-phantomjs:$PATH
+
+before_script:
+ - sh script/ci_setup.sh $DB
+
+script:
+ - sh script/ci_runner.sh $TEST_SUITE $GROUP_SIZE $GROUP
+
+notifications:
+ email: false
+ slack:
+ secure: "a+I0uMgXgrDd3aitr2yhXrh7g/UOUTwoDVElunY7gYdrM+gpZ6RE1AP4/Q++hERBCs7rUBzmb//zxGTcc8Nw4nGqZOmPOMIsAoD49UupGLUzHbxzKlpwdBcwh77fq3rYwkjZjE/H1qiElPT7v6qyWMSdNGlj/bAB74eD7Zl3S5cMRvZ1whbSg2GA2v6ZqtXaKfrSFrPRzsIOBXs99OxWNWAsUGpEwTYac7wb6rdMJkBbzosp4gP99mGvQArEzo0nrIQgRH8W4Q6iLnrpX0g5uKccWl1u/G2bmH8L4F50ce4uuUE+TtHO/nfNFnb2KuDR4QyoccQQbGHXL/jaaAZXG/gzs5Hmru2Thaym43fSwxos80xmZs1vqB/rXE+Rg9qXcCKyyX31zjSI/iW4wS015fz8MKVX6qDg49epaw1ovn0AOYrvTd94GV6RX6eJ3/l+KJJHSKaaLP/713h11LWx/S27tiB40fboXQ68YzIQCuahRUEHUfhU3P10Wf9y2fdDsthtHHSrOJMQ3Ii/Jm3KQm6bE5RWORdHvc/sF2WLfLmJ627j9JhWYYi5mDKJ9AeMWtZNHreU0mM27OUgfhiW11ItKgpwQPEiiicrlYRrMmK+9hc9cym+8tRM+wEth1xhIkfgQFtngONKjv361Wt3JifxM79+bn0IyF72vAVNy8k="
+
+addons:
+ firefox: "38.0esr"
+ postgresql: "9.3"
+
+# Disabling coverage reporting until CodeClimate supports merging results from multiple partial tests
+# code_climate:
+# repo_token:
+# secure: "W/lyd8Ud18GRASuVShsIKa2MRHhxjh8WICMQ4WKr68qt0X0Tlp7Bclv4ReiEgiQeKsIoJJy5FfJfINdAT8A4sy2JbrLeISShcIU7Kqpfh6DSLNoRAuLz5P7EXMNFns1gBKCmrSzcB+9ksuTLyTCKkjUcj1NbJzGqpB4jSTecAdg="
\ No newline at end of file
diff --git a/vendored-plugins/openproject-global_roles/Gemfile.plugins b/vendored-plugins/openproject-global_roles/Gemfile.plugins
new file mode 100644
index 0000000000..ecbfb20364
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/Gemfile.plugins
@@ -0,0 +1,7 @@
+# Used by travis to bundle this plugin with the OpenProject core.
+# The tested plugin will be moved to the path `./plugins/this`
+# whereas OpenProject will be checked out to `.`.
+
+gem 'openproject-global_roles', path: 'plugins/this'
+
+# If the plugin has any dependencies declare them here:
\ No newline at end of file
diff --git a/vendored-plugins/openproject-global_roles/README.md b/vendored-plugins/openproject-global_roles/README.md
new file mode 100644
index 0000000000..ea96c1fab7
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/README.md
@@ -0,0 +1,84 @@
+OpenProject Global Roles Plugin
+==========================
+
+This plugin adds global roles to [OpenProject](https://www.openproject.org).
+A user can have a global role allowing to perform actions outside of the scope
+of a specific project normally only allowed for administrators.
+By assigning the permission to create projects to a global role,
+non-administrators can create top-level projects.
+
+To create a global role, go to the Administration view, select "Roles and permissions"
+and click on "New Role". Afterwards, select the checkbox "Global Role" and choose the
+permissions for this role. From the plugin, only "Create project" is available.
+However, there are other plugins adding more permissions to be assigned to global
+roles.
+
+The created global roles can be assigned to individual users in the added "Global Roles"
+tab of the user settings.
+
+Requirements
+------------
+
+The Global Roles plugin currently requires the [OpenProject Core](https://github.com/opf/openproject/) in
+version 3.0.0 or newer.
+
+
+Installation
+------------
+
+For OpenProject Global Roles itself you need to add the following line to the `Gemfile.plugins` of OpenProject (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-global_roles", git: "https://github.com/finnlabs/openproject-global_roles.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+Deinstallation
+--------------
+
+Currently, a complete automatic uninstall is not supported.
+Before the plugin can be removed, all global roles have to be deleted.
+Afterwards, remove the line
+
+`gem "openproject-global_roles", git: "https://github.com/finnlabs/openproject-global_roles.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this changes by the plugin in the database. Currently, we do not
+support full uninstall of the plugin.
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/plugin-global-roles
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+
+`https://github.com/finnlabs/openproject-global_roles`
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+
+License
+-------
+
+(c) 2010 - 2014 - Finn GmbH
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.rdoc and
+doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/global_roles.js b/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/global_roles.js
new file mode 100644
index 0000000000..1aa0102e09
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/global_roles.js
@@ -0,0 +1,111 @@
+//-- copyright
+// OpenProject Global Roles Plugin
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+//= require global_roles/principal_roles
+
+(function ($, undefined) {
+ var global_roles = {
+ init: function(){
+ if (global_roles.script_applicable()) {
+ global_roles.toggle_forms_on_click();
+ global_roles.activation_and_visibility_based_on_checked($('#global_role'));
+ }
+ },
+
+ toggle_forms_on_click: function(){
+ $('#global_role').on("click", global_roles.toggle_forms);
+ },
+
+ toggle_forms: function(event){
+ global_roles.activation_and_visibility_based_on_checked(this)
+ },
+
+ activation_and_visibility_based_on_checked: function(element){
+ if($(element).attr("checked")){
+ global_roles.show_global_forms();
+ global_roles.hide_member_forms();
+ global_roles.enable_global_forms();
+ global_roles.disable_member_forms();
+ }
+ else{
+ global_roles.show_member_forms();
+ global_roles.hide_global_forms();
+ global_roles.disable_global_forms();
+ global_roles.enable_member_forms();
+ }
+ },
+
+ show_global_forms: function(){
+ $('#global_attributes').show();
+ $('#global_permissions').show();
+ },
+
+ show_member_forms: function(){
+ $('#member_attributes').show();
+ $('#member_permissions').show();
+ },
+
+ hide_global_forms: function(){
+ $('#global_attributes').hide();
+ $('#global_permissions').hide();
+ },
+
+ hide_member_forms: function(){
+ $('#member_attributes').hide();
+ $('#member_permissions').hide();
+ },
+
+ enable_global_forms: function(){
+ $('#global_attributes input, #global_attributes input, #global_permissions input').each(function (ix, el) {
+ global_roles.enable_element(el);
+ });
+ },
+
+ enable_member_forms: function(){
+ $('#member_attributes input, #member_attributes input, #member_permissions input').each(function (ix, el) {
+ global_roles.enable_element(el);
+ });
+ },
+
+ enable_element: function(element){
+ element.enable();
+ },
+
+ disable_global_forms: function(){
+ $('#global_attributes input, #global_attributes input, #global_permissions input').each(function (ix, el) {
+ global_roles.disable_element(el);
+ });
+ },
+
+ disable_member_forms: function(){
+ $('#member_attributes input, #member_attributes input, #member_permissions input').each(function (ix, el) {
+ global_roles.disable_element(el);
+ });
+ },
+
+ disable_element: function(element){
+ element.disable();
+ },
+
+ script_applicable: function() {
+ return $('body.controller-roles.action-new, body.controller-roles.action-create').size() === 1;
+ }
+ }
+ $(document).ready(global_roles.init);
+}(jQuery));
diff --git a/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/principal_roles.js b/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/principal_roles.js
new file mode 100644
index 0000000000..6a0d174872
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/assets/javascripts/global_roles/principal_roles.js
@@ -0,0 +1,55 @@
+//-- copyright
+// OpenProject Global Roles Plugin
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+(function ($, undefined) {
+ var principal_roles = {
+ init: function(){
+ principal_roles.set_table_visibility();
+ principal_roles.set_available_roles_visibility();
+ },
+
+ set_table_visibility: function(){
+ if ($('#table_principal_roles_body tr').length > 0){
+ $('#tab-content-global_roles .generic-table--results-container').show();
+ $('#tab-content-global_roles .generic-table--no-results-container').hide();
+ }
+ else
+ {
+ $('#tab-content-global_roles .generic-table--results-container').hide();
+ $('#tab-content-global_roles .generic-table--no-results-container').show();
+ }
+ },
+
+ set_available_roles_visibility: function(){
+ if ($('.principal_role_option').length > 0){
+ $('#additional_principal_roles').show();
+ $('#no_additional_principal_roles').hide();
+ }
+ else
+ {
+ $('#additional_principal_roles').hide();
+ $('#no_additional_principal_roles').show();
+ }
+ }
+ };
+
+ $(document).ready(function () {
+ $(document).ajaxStop(principal_roles.init);
+ principal_roles.init();
+ });
+}(jQuery));
diff --git a/vendored-plugins/openproject-global_roles/app/controllers/principal_roles_controller.rb b/vendored-plugins/openproject-global_roles/app/controllers/principal_roles_controller.rb
new file mode 100644
index 0000000000..990098631d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/controllers/principal_roles_controller.rb
@@ -0,0 +1,152 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class PrincipalRolesController < ApplicationController
+ def create
+ @principal_roles = new_principal_roles_from_params
+ @global_roles = GlobalRole.all
+ @user = Principal.find(principle_role_params[:principal_id])
+
+ call_hook :principal_roles_controller_create_before_save,
+ principal_roles: @principal_roles
+
+ @principal_roles.each(&:save) unless performed?
+
+ call_hook :principal_roles_controller_create_before_respond,
+ principal_roles: @principal_roles
+
+ respond_to_create @principal_roles, @user, @global_roles unless performed?
+ end
+
+ def update
+ @principal_role = PrincipalRole.find(principle_role_params[:id])
+
+ call_hook :principal_roles_controller_update_before_save,
+ principal_role: @principal_role
+
+ @principal_role.update_attributes(principle_role_params) unless performed?
+
+ call_hook :principal_roles_controller_update_before_respond,
+ principal_role: @principal_role
+
+ respond_to_update @principal_role unless performed?
+ end
+
+ def destroy
+ @principal_role = PrincipalRole.find(params[:id])
+ @user = Principal.find(@principal_role.principal_id)
+ @global_roles = GlobalRole.all
+
+ call_hook :principal_roles_controller_destroy_before_destroy,
+ principal_role: @principal_role
+
+ @principal_role.destroy unless performed?
+
+ call_hook :principal_roles_controller_destroy_before_respond,
+ principal_role: @principal_role
+
+ respond_to_destroy @principal_role, @user, @global_roles unless performed?
+ end
+
+ private
+
+ def new_principal_roles_from_params
+ pr_params = principle_role_params.dup
+ role_ids = pr_params[:role_id] ? [pr_params.delete(:role_id)] : pr_params.delete(:role_ids)
+ principal_id = pr_params.delete(:principal_id)
+
+ roles = Role.find role_ids
+
+ principal_roles = []
+ role_ids.map(&:to_i).each do |role_id|
+ role = PrincipalRole.new(pr_params)
+ role.principal_id = principal_id
+ role.role = roles.detect { |r| r.id == role_id }
+ principal_roles << role
+ end
+ principal_roles
+ end
+
+ def respond_to_create(principal_roles, user, global_roles)
+ respond_to do |format|
+ format.js do
+ render(:update) do |page|
+ if principal_roles.all?(&:valid?)
+ principal_roles.each do |role|
+ page.insert_html :top, 'table_principal_roles_body',
+ partial: 'principal_roles/show_table_row',
+ locals: { principal_role: role }
+
+ call_hook :principal_roles_controller_create_respond_js_role,
+ page: page, principal_role: role
+ end
+
+ page.replace 'available_principal_roles',
+ partial: 'users/available_global_roles',
+ locals: { global_roles: global_roles,
+ user: user }
+ else
+ page.insert_html :top, 'tab-content-global_roles', partial: 'errors'
+ end
+ end
+ end
+ end
+ end
+
+ def respond_to_update(role)
+ respond_to do |format|
+ format.js do
+ render(:update) do |page|
+ if role.valid?
+ page.replace "principal_role-#{role.id}",
+ partial: 'principal_roles/show_table_row',
+ locals: { principal_role: role }
+ else
+ page.insert_html :top, 'tab-content-global_roles', partial: 'errors'
+ end
+
+ call_hook :principal_roles_controller_update_respond_js_role,
+ page: page, principal_role: role
+ end
+ end
+ end
+ end
+
+ def respond_to_destroy(principal_role, user, global_roles)
+ respond_to do |format|
+ format.js do
+ render(:update) do |page|
+ page.remove "principal_role-#{principal_role.id}"
+ page.replace 'available_principal_roles',
+ partial: 'users/available_global_roles',
+ locals: { user: user, global_roles: global_roles }
+
+ call_hook :principal_roles_controller_update_respond_js_role,
+ page: page, principal_role: principal_role
+ end
+ end
+ end
+ end
+
+ private
+
+ def principle_role_params
+ params.require(:principal_role).permit(*PermittedParams.permitted_attributes[:global_roles_principal_role])
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/app/models/global_role.rb b/vendored-plugins/openproject-global_roles/app/models/global_role.rb
new file mode 100644
index 0000000000..3144d849b5
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/models/global_role.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class GlobalRole < Role
+ has_many :principal_roles, foreign_key: :role_id, dependent: :destroy
+ has_many :principals, through: :principal_roles
+
+ def initialize(*args)
+ super
+ self.assignable = false
+ end
+
+ def permissions=(perms)
+ perms = perms.collect { |p| p.to_sym unless p.blank? }.compact.uniq if perms
+ write_attribute(:permissions, perms)
+ end
+
+ def setable_permissions
+ Redmine::AccessControl.global_permissions
+ end
+
+ def self.setable_permissions
+ Redmine::AccessControl.global_permissions
+ end
+
+ def to_s
+ name
+ end
+
+ def assignable=(value)
+ fail ArgumentError if value == true
+ super
+ end
+
+ def assignable_to?(_user)
+ true
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/app/models/principal_role.rb b/vendored-plugins/openproject-global_roles/app/models/principal_role.rb
new file mode 100644
index 0000000000..a7663ef091
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/models/principal_role.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class PrincipalRole < ActiveRecord::Base
+ belongs_to :principal
+ belongs_to :role
+ validate :validate_assignable
+
+ def validate_assignable
+ add_error_can_not_be_assigned unless role.assignable_to?(principal)
+ end
+
+ private
+
+ def add_error_can_not_be_assigned
+ errors[:base] << l(:error_can_not_be_assigned)
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/app/views/principal_roles/_errors.html.erb b/vendored-plugins/openproject-global_roles/app/views/principal_roles/_errors.html.erb
new file mode 100644
index 0000000000..0671f4ca0d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/principal_roles/_errors.html.erb
@@ -0,0 +1,21 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= error_messages_for 'principal_role' %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/principal_roles/_show_table_row.html.erb b/vendored-plugins/openproject-global_roles/app/views/principal_roles/_show_table_row.html.erb
new file mode 100644
index 0000000000..60519d0e24
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/principal_roles/_show_table_row.html.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+
+ <%=h principal_role.role %>
+
+<%= call_hook(:principal_roles_table_row, :user => @user, :principal_role => principal_role )%>
+
+<% buttons = {:edit => false} %>
+<%= call_hook(:principal_roles_edit_button_needed, :buttons => buttons) %>
+<% if buttons[:edit]%>
+ <%= link_to_function l(:button_edit), "return false;", :class => 'icon icon-edit' %>
+<%end%>
+ <%= link_to(l(:button_delete),
+ principal_role,
+ { method: :delete, remote: true, class: 'icon icon-delete' }) %>
+
+
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_form.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_form.html.erb
new file mode 100644
index 0000000000..45e7c755d5
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_form.html.erb
@@ -0,0 +1,60 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/global_roles_header' %>
+
+<% roles ||= nil %>
+
+<%= error_messages_for :role %>
+
+ <%= f.text_field :name, required: true, container_class: '-slim' %>
+
+
+ <% if role.new_record? %>
+ <%= styled_label_tag :global_role, l(:label_global_role) %>
+ <%= styled_check_box_tag("global_role", "1", role.is_a?(GlobalRole))%>
+ <% else %>
+ <%= styled_label_tag :unchangeable, "#{l(:label_role_type)} #{l(:label_not_changeable)}" %>
+ <%= (role.is_a?(GlobalRole) ? l(:label_global_role) : l(:label_member_role))%>
+ <% end %>
+
+
+<% if role.new_record? || role.is_a?(GlobalRole) %>
+
+ <%= render partial: "global_attributes", locals: { f: f, role: role, roles: (roles.present? ? roles.select {|r| r.is_a?(GlobalRole)} : nil) }%>
+
+<% end %>
+
+<% if role.new_record? || !role.is_a?(GlobalRole) %>
+
+ <%= render partial: "member_attributes", locals: { f: f, role: role, roles: (roles.present? ? roles.select {|r| !r.is_a?(GlobalRole)} : nil)}%>
+
+<% end %>
+
+<% if role.new_record? || role.is_a?(GlobalRole) %>
+ >
+ <%= render partial: "permissions", locals: {permissions: global_permissions, role: role, globalRole: "true" }%>
+
+<% end %>
+<% if role.new_record? || !role.is_a?(GlobalRole) %>
+ >
+ <%= render partial: "permissions", locals: {permissions: member_permissions, role: role, globalRole: "false" }%>
+
+<% end %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_global_attributes.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_global_attributes.html.erb
new file mode 100644
index 0000000000..d5a3827671
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_global_attributes.html.erb
@@ -0,0 +1,19 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_global_form.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_global_form.html.erb
new file mode 100644
index 0000000000..d5a3827671
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_global_form.html.erb
@@ -0,0 +1,19 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_member_attributes.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_member_attributes.html.erb
new file mode 100644
index 0000000000..b3af6b1df4
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_member_attributes.html.erb
@@ -0,0 +1,29 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+ <%= f.check_box :assignable %>
+
+<% if role.new_record? && roles.any? %>
+
+ <%= styled_label_tag :copy_workflow_from, l(:label_copy_workflow_from) %>
+ <%= styled_select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles.select{|r| !r.is_a?(GlobalRole)}, :id, :name), container_class: '-slim') %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_member_form.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_member_form.html.erb
new file mode 100644
index 0000000000..51dcf2f440
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_member_form.html.erb
@@ -0,0 +1,51 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% unless role.builtin? %>
+
+ <%= f.text_field :name, required: true %>
+
+
+ <%= f.check_box :assignable %>
+
+ <% if role.new_record? && roles.any? %>
+
+ <%= styled_label_tag :copy_workflow_from, l(:label_copy_workflow_from) %>
+ <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name)) %>
+
+ <% end %>
+<% end %>
+
+<%= l(:label_permissions) %>
+
+ <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
+ <% perms_by_module.keys.sort.each do |mod| %>
+
<%= mod.blank? ? Project.model_name.human : l_or_humanize(mod, :prefix => 'project_module_') %>
+ <% perms_by_module[mod].each do |permission| %>
+
+ <%= styled_check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name) %>
+ <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
+
+ <% end %>
+
+ <% end %>
+ <%= check_all_links 'permissions' %>
+ <%= hidden_field_tag 'role[permissions][]', '' %>
+
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/_permissions.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/_permissions.html.erb
new file mode 100644
index 0000000000..7a6e5aa3a4
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/_permissions.html.erb
@@ -0,0 +1,52 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% perms_by_module = permissions.group_by {|p| p.project_module.to_s} %>
+<% perms_by_module.keys.sort.each do |mod| %>
+ <% if globalRole === 'false' %>
+ <% module_name = mod.blank? ? 'fieldset--' + Project.model_name.human.downcase.gsub(' ', '_') : 'fieldset--' + l_or_humanize(mod, prefix: 'project_module_').downcase.gsub(' ', '_') %>
+
+ <% else %>
+ <% module_name = mod.blank? ? 'fieldset--global--' + Project.model_name.human.downcase.gsub(' ', '_') : 'fieldset--global--' + l_or_humanize(mod, prefix: 'project_module_').downcase.gsub(' ', '_') %>
+
+ <% end %>
+
+
+ <%= mod.blank? ? Project.model_name.human : l_or_humanize(mod, prefix: 'project_module_') %>
+
+
+
+ (<%= check_all_links module_name %>)
+
+
+ <% perms_by_module[mod].each do |permission| %>
+
+
+ <%= check_box_tag 'role[permissions][]', permission.name, (role.permissions && role.permissions.include?(permission.name)) %>
+ <%= l_or_humanize(permission.name, prefix: 'permission_') %>
+
+
+ <% end %>
+
+
+
+
+<% end %>
+<%= hidden_field_tag 'role[permissions][]', '' %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/edit.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/edit.html.erb
new file mode 100644
index 0000000000..49adbcbecd
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/edit.html.erb
@@ -0,0 +1,31 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% html_title l(:label_administration), "#{l(:label_edit)} #{Role.model_name.human} #{@role.name}" %>
+
+<%= breadcrumb_toolbar link_to(Role.model_name.human(:count => 2), roles_path), @role.name %>
+
+<%= labelled_tabular_form_for @role, :url => { :action => 'update' }, :html => {:id => 'role_form'}, :as => :role do |f| %>
+<%= hidden_field_tag :id, @role.id %>
+<%= render :partial => 'form', :locals => { :f => f , :role => @role, :member_permissions => (@role.is_a?(GlobalRole) ? nil : @permissions),
+:global_permissions => (@role.is_a?(GlobalRole) ? @permissions : nil)} %>
+
+<%= styled_button_tag l(:button_save), class: '-with-icon icon-checkmark' %>
+<% end %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/index.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/index.html.erb
new file mode 100644
index 0000000000..0f5d0813ff
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/index.html.erb
@@ -0,0 +1,100 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+<% html_title l(:label_administration), l("label_role_plural") %>
+<%= toolbar title: Role.model_name.human(count: 2) do %>
+
+ <%= link_to({ action: 'new'}, class: 'button -alt-highlight') do %>
+ <%= l(:label_role_new) %>
+ <% end %>
+
+<% end %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% for role in @roles %>
+
+
+ <%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, :action => 'edit', :id => role)) %>
+
+
+ <%= icon_wrapper('icon-context icon-checkmark', I18n.t(:general_text_Yes)) if role.is_a?(GlobalRole) %>
+
+
+ <% unless role.builtin? %>
+ <%= reorder_links('role', {:action => 'update', :id => role}, :method => :put) %>
+ <% end %>
+
+
+ <%= link_to(l(:button_delete), role_path(role),
+ :method => :delete,
+ :confirm => l(:text_are_you_sure),
+ :class => 'icon icon-delete') unless role.builtin? %>
+
+
+ <% end %>
+
+
+
+
+
+
+<%= pagination_links_full @roles %>
+
+<%= link_to l(:label_permissions_report), :action => 'report' %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/roles/new.html.erb b/vendored-plugins/openproject-global_roles/app/views/roles/new.html.erb
new file mode 100644
index 0000000000..3c545d8329
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/roles/new.html.erb
@@ -0,0 +1,29 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% html_title l(:label_administration), l("label_group_new") %>
+
+<%= breadcrumb_toolbar link_to(Role.model_name.human(:count => 2), roles_path), l(:label_role_new) %>
+
+<%= labelled_tabular_form_for @role, :url => { :action => 'create' }, :html => {:id => 'role_form'}, :as => :role do |f| %>
+<%= render :partial => 'form', :locals => { :f => f, :role => @role, :member_role => @member_role, :global_role => @global_role, :roles => @roles, :member_permissions => @member_permissions, :global_permissions => @global_permissions} %>
+
+<%= styled_button_tag l(:button_create), class: '-with-icon icon-checkmark' %>
+<% end %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/shared/_global_roles_header.html.erb b/vendored-plugins/openproject-global_roles/app/views/shared/_global_roles_header.html.erb
new file mode 100644
index 0000000000..83a1a4cf7c
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/shared/_global_roles_header.html.erb
@@ -0,0 +1,23 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= javascript_include_tag 'global_roles/global_roles.js' %>
+<% end %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/users/_available_global_role.html.erb b/vendored-plugins/openproject-global_roles/app/views/users/_available_global_role.html.erb
new file mode 100644
index 0000000000..169a065814
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/users/_available_global_role.html.erb
@@ -0,0 +1,22 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
+<%= check_box_tag 'principal_role[role_ids][]', role.id, false, :id => "principal_role_role_ids_#{role.id}", :disabled => !role.assignable_to?(@user) %><%= label_tag "principal_role_role_ids_#{role.id}", h(role) %>
diff --git a/vendored-plugins/openproject-global_roles/app/views/users/_available_global_roles.html.erb b/vendored-plugins/openproject-global_roles/app/views/users/_available_global_roles.html.erb
new file mode 100644
index 0000000000..1fc8f7102d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/users/_available_global_roles.html.erb
@@ -0,0 +1,40 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% def available_additional_global_roles available_roles, user
+ available_roles - user.global_roles(true)
+end%>
+
+
+
<%= Role.model_name.human(:count => 2) %>
+
+<%= form_for(:principal_roles, :url => principal_roles_path, :method => :post, :remote => :true) do %>
+ <%= hidden_field_tag 'principal_role[principal_id]', user.id %>
+<% available_additional_global_roles(global_roles, user).each do |role| %>
+ <%= render :partial => 'users/available_global_role', :locals => {:role => role} %>
+<% end %>
+ <%= styled_button_tag l(:button_add), class: '-with-icon icon-checkmark' %>
+<% end %>
+
+
+<%= l(:label_no_assignable_role) %>
+
+
+
diff --git a/vendored-plugins/openproject-global_roles/app/views/users/_global_roles.html.erb b/vendored-plugins/openproject-global_roles/app/views/users/_global_roles.html.erb
new file mode 100644
index 0000000000..fe3485c06a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/app/views/users/_global_roles.html.erb
@@ -0,0 +1,68 @@
+<%#-- copyright
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'shared/global_roles_header' %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= call_hook(:view_users_global_roles_table_header, :user => @user )%>
+
+
+
+
+ <% @user.principal_roles.each do |principal_role| %>
+ <%= render :partial => 'principal_roles/show_table_row', :locals => {:principal_role => principal_role} %>
+ <%end%>
+
+
+
+
+
+
+
+ <%= l(:label_nothing_display) %>
+
+
+
<%= l(:label_no_data) %>
+
+
+
+
+
+ <%= render :partial => 'users/available_global_roles', :locals => {:user => @user, :global_roles => @global_roles}%>
+
diff --git a/vendored-plugins/openproject-global_roles/config/locales/de.yml b/vendored-plugins/openproject-global_roles/config/locales/de.yml
new file mode 100644
index 0000000000..099e957e26
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/de.yml
@@ -0,0 +1,9 @@
+de:
+ error_can_not_be_assigned: Die Rolle kann diesem Nutzer nicht zugewiesen werden
+ global_roles: Globale Rollen
+ label_role_type: Typ
+ label_member_role: Projekt Rolle
+ label_global_role: Globale Rolle
+ label_not_changeable: (nicht veränderbar)
+ label_no_assignable_role: Keine globale Rolle zuweisbar
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/locales/en.yml b/vendored-plugins/openproject-global_roles/config/locales/en.yml
new file mode 100644
index 0000000000..5b036d638a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/en.yml
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+en:
+ error_can_not_be_assigned: The role can not be assigned to this user
+
+ global_roles: Global Roles
+
+ label_role_type: Type
+ label_member_role: Project Role
+ label_global_role: Global Role
+ label_not_changeable: (not changeable)
+ label_no_assignable_role: No global role available for assignment
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/locales/fr.yml b/vendored-plugins/openproject-global_roles/config/locales/fr.yml
new file mode 100644
index 0000000000..8503a523c9
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/fr.yml
@@ -0,0 +1,9 @@
+fr:
+ error_can_not_be_assigned: Le rôle ne peut pas être assigné à cet utilisateur
+ global_roles: Rôles globaux
+ label_role_type: Type
+ label_member_role: Rôle du projet
+ label_global_role: Rôle global
+ label_not_changeable: (non modifiable)
+ label_no_assignable_role: "Aucun rôle global disponible pour l'assignation"
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/locales/it.yml b/vendored-plugins/openproject-global_roles/config/locales/it.yml
new file mode 100644
index 0000000000..25484c1f8a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/it.yml
@@ -0,0 +1,9 @@
+it:
+ error_can_not_be_assigned: Il ruolo non può essere assegnato a questo utente
+ global_roles: Ruoli globali
+ label_role_type: Tipo
+ label_member_role: Ruolo di progetto
+ label_global_role: Ruolo globale
+ label_not_changeable: (non modificabile)
+ label_no_assignable_role: "Nessun ruolo globale disponibile per l'assegnazione"
+ label_global: Globale
diff --git a/vendored-plugins/openproject-global_roles/config/locales/pt-BR.yml b/vendored-plugins/openproject-global_roles/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..df8909daf5
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/pt-BR.yml
@@ -0,0 +1,9 @@
+pt-BR:
+ error_can_not_be_assigned: O papel não pode ser atribuído a este usuário
+ global_roles: Papéis globais
+ label_role_type: Tipo
+ label_member_role: Papel no projeto
+ label_global_role: Papel global
+ label_not_changeable: (não modificável)
+ label_no_assignable_role: Nenhum papel global disponível para atribuição
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/locales/ru.yml b/vendored-plugins/openproject-global_roles/config/locales/ru.yml
new file mode 100644
index 0000000000..970faf5d44
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/ru.yml
@@ -0,0 +1,9 @@
+ru:
+ error_can_not_be_assigned: Роль не может быть назначена для этого пользователя
+ global_roles: Глобальная роль
+ label_role_type: Тип
+ label_member_role: Роль проекта
+ label_global_role: Глобальная роль
+ label_not_changeable: (неизменяемая)
+ label_no_assignable_role: Нет глобальной роли, доступной для назначения
+ label_global: Глобальная
diff --git a/vendored-plugins/openproject-global_roles/config/locales/sk.yml b/vendored-plugins/openproject-global_roles/config/locales/sk.yml
new file mode 100644
index 0000000000..6402f0a7e7
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/sk.yml
@@ -0,0 +1,9 @@
+sk:
+ error_can_not_be_assigned: Rola nemôže byť priradená tomuto používateľovi
+ global_roles: Globálne roly
+ label_role_type: Typ
+ label_member_role: Rola projektu
+ label_global_role: Globálna rola
+ label_not_changeable: (nemodifikovateľná)
+ label_no_assignable_role: Nie je dostupná žiadna globálna rola pre priradenie
+ label_global: Globálna
diff --git a/vendored-plugins/openproject-global_roles/config/locales/sv-SE.yml b/vendored-plugins/openproject-global_roles/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..930f509513
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/sv-SE.yml
@@ -0,0 +1,9 @@
+sv:
+ error_can_not_be_assigned: Rollen kan inte tilldelas till denna användare
+ global_roles: Globala roller
+ label_role_type: Typ
+ label_member_role: Projektroll
+ label_global_role: Globalroll
+ label_not_changeable: (kan inte ändras)
+ label_no_assignable_role: Ingen global roll tillgänglig för tilldelning
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/locales/tr.yml b/vendored-plugins/openproject-global_roles/config/locales/tr.yml
new file mode 100644
index 0000000000..f277657ba8
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/locales/tr.yml
@@ -0,0 +1,9 @@
+tr:
+ error_can_not_be_assigned: Bu kullanıcı için rol atanabilir değil
+ global_roles: Global Roles
+ label_role_type: Type
+ label_member_role: Proje rolü
+ label_global_role: Global Role
+ label_not_changeable: (değiştirilemez)
+ label_no_assignable_role: No global role available for assignment
+ label_global: Global
diff --git a/vendored-plugins/openproject-global_roles/config/routes.rb b/vendored-plugins/openproject-global_roles/config/routes.rb
new file mode 100644
index 0000000000..badfc337f9
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/config/routes.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+OpenProject::Application.routes.draw do
+ resources :principal_roles
+end
diff --git a/vendored-plugins/openproject-global_roles/db/migrate/20100528100562_aggregated_global_roles_migrations.rb b/vendored-plugins/openproject-global_roles/db/migrate/20100528100562_aggregated_global_roles_migrations.rb
new file mode 100644
index 0000000000..5664f2875a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/db/migrate/20100528100562_aggregated_global_roles_migrations.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'migration_squasher').to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedGlobalRolesMigrations < ActiveRecord::Migration
+ MIGRATION_FILES = <<-MIGRATIONS
+ 001_sti_for_roles.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = 'redmine_global_roles'
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ add_column :roles, :type, :string, limit: 30, default: 'Role'
+
+ ActiveRecord::Base.connection.execute("UPDATE roles SET type='Role';")
+
+ Role.reset_column_information
+
+ create_table :principal_roles do |t|
+ t.column :role_id, :integer, null: false
+ t.column :principal_id, :integer, null: false
+ t.timestamps
+ end
+
+ add_index :principal_roles, :role_id
+ add_index :principal_roles, :principal_id
+ end
+ end
+
+ def down
+ remove_column :roles, :type
+ remove_index :principal_roles, :role_id
+ remove_index :principal_roles, :principal_id
+ drop_table :principal_roles
+ Role.reset_column_information
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/doc/CHANGELOG.md b/vendored-plugins/openproject-global_roles/doc/CHANGELOG.md
new file mode 100644
index 0000000000..ccaa5496ab
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/doc/CHANGELOG.md
@@ -0,0 +1,76 @@
+
+
+# Changelog
+
+* `#8374` Global Roles associated permissions become invisible
+
+## 1.0.2
+
+* `#5357` Adapt released plugins to base on plugins functionality
+
+## 1.0.1
+
+* `#4024` Subpages have no unique page titles
+* `#4255` Fix: Permissions not displayed when editing a global role
+
+## 1.0.1.pre5
+
+* `#3333` [CodeClimate] Mass Assignment RolesController
+* `#2256` [Accessibility] linearisation of issue show form (2)
+* Rewritten Javascript to exclusively use jQuery
+
+## 1.0.1.pre4
+
+* `#2154` [Accessibility] Link form elements to their label - new role
+
+## 1.0.1.pre3
+
+* Adaptations for new icon font
+
+## 1.0.1.pre2
+
+* `#2459` Squashed old migrations
+
+## 1.0.1.pre1
+
+* `#1797` Adaptions to Migration of DTAG Customizing
+* `#2070` Adaptions for moving core assets to other locations
+
+## 1.0.0
+
+* `#1643` Public Release of Global Roles Plugin
+
+## 1.0.0.pre4
+
+* adapted cukes to changed steps from core
+
+## 1.0.0.pre3
+
+* `#1461` Fix for: Integration Activity Plugin
+
+## 1.0.0.pre2
+
+* '#1319' Fix for failures on CI
+
+## 1.0.0.pre1
+
+* First rails 3 version
+* `#1284` Created initial changelog
diff --git a/vendored-plugins/openproject-global_roles/doc/COPYRIGHT.md b/vendored-plugins/openproject-global_roles/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..727dcd78ee
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject Global Roles Plugin
+
+This Plugin adds global roles to OpenProject
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-global_roles/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-global_roles/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..0103718ba1
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/doc/COPYRIGHT_short.md
@@ -0,0 +1,16 @@
+OpenProject Global Roles Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-global_roles/doc/GPL.txt b/vendored-plugins/openproject-global_roles/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-global_roles/features/global_create_project.feature b/vendored-plugins/openproject-global_roles/features/global_create_project.feature
new file mode 100644
index 0000000000..dd43c7b3f0
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/global_create_project.feature
@@ -0,0 +1,72 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Global Create Project
+
+ Scenario: Create Project is not a member permission
+ Given there is a role "Member"
+ And I am already admin
+ When I go to the edit page of the role "Member"
+ Then I should not see "Create project"
+
+ Scenario: Create Project is a global permission
+ Given there is a global role "Global"
+ And I am already admin
+ When I go to the edit page of the role "Global"
+ Then I should see "Create project"
+
+ Scenario: Create Project displayed to user
+ Given there is a global role "Global"
+ And the global role "Global" may have the following rights:
+ | add_project |
+ And there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ And the user "bob" has the global role "Global"
+ When I am already logged in as "bob"
+ And I go to the overall projects page
+ Then I should see "New project"
+
+ Scenario: Create Project not displayed to user without global role
+ Given there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ When I am already logged in as "bob"
+ And I go to the overall projects page
+ Then I should not see "New project"
+
+ @javascript
+ Scenario: Create Project displayed to user
+ Given there is a global role "Global"
+ And the global role "Global" may have the following rights:
+ | add_project |
+ And there is a role "Manager"
+ And there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ And the user "bob" has the global role "Global"
+ When I am already logged in as "bob"
+ And I go to the new page of "Project"
+ And I fill in "project_name" with "ProjectName"
+ And I press "Create"
+ Then I should see "Successful creation."
+ And I should be on the settings page of the project called "ProjectName"
diff --git a/vendored-plugins/openproject-global_roles/features/global_role_assignment.feature b/vendored-plugins/openproject-global_roles/features/global_role_assignment.feature
new file mode 100644
index 0000000000..b15e7ed298
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/global_role_assignment.feature
@@ -0,0 +1,79 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Global Role Assignment
+
+ @javascript
+ Scenario: Going to the global role assignment page
+ Given there is the global permission "global1" of the module "global"
+ And there is the global permission "global2" of the module "global"
+ And there is a global role "global_role1"
+ And there is a global role "global_role2"
+ And the global role "global_role1" may have the following rights:
+ | global1 |
+ And the global role "global_role2" may have the following rights:
+ | global2 |
+ And there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ And the user "bob" has the global role "global_role1"
+ And I am already admin
+ When I go to the edit page of the user called "bob"
+ And I click on "tab-global_roles"
+ Then I should see "global_role1" within "#table_principal_roles"
+ And I should not see "global_role1" within "#available_principal_roles"
+ And I should see "global_role2" within "#available_principal_roles"
+
+ @javascript
+ Scenario: Assigning a global role to a user
+ Given there is the global permission "global1" of the module "global"
+ And there is a global role "global_role"
+ And the global role "global_role" may have the following rights:
+ | global1 |
+ And there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ And I am already admin
+ When I go to the edit page of the user called "bob"
+ And I click on "tab-global_roles"
+ And I select the available global role "global_role"
+ And I press "Add"
+ Then I should see "global_role" within "#table_principal_roles"
+ And I should not see "global_role" within "#available_principal_roles"
+ And I should see "No global role available for assignment"
+
+ @javascript
+ Scenario: Deleting a global role of a user
+ Given there is the global permission "global1" of the module "global"
+ And there is a global role "global_role"
+ And the global role "global_role" may have the following rights:
+ | global1 |
+ And there is 1 User with:
+ | Login | bob |
+ | Firstname | Bob |
+ | Lastname | Bobbit |
+ And the user "bob" has the global role "global_role"
+ And I am already admin
+ When I go to the edit page of the user called "bob"
+ And I click on "tab-global_roles"
+ And I delete the assigned role "global_role"
+ Then I should see "Nothing to display" within "#assigned_principal_roles"
+ And I should see "global_role" within "#available_principal_roles"
diff --git a/vendored-plugins/openproject-global_roles/features/global_role_crud.feature b/vendored-plugins/openproject-global_roles/features/global_role_crud.feature
new file mode 100644
index 0000000000..9732cecca2
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/global_role_crud.feature
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: As an admin
+ I want to administrate global roles with permissions
+ So that I can modify permission groups
+
+ @javascript
+ Scenario: Global Role creation
+ Given there is the global permission "glob_test" of the module "global_group"
+ And I am already admin
+ When I go to the new page of "Role"
+ Then I should not see block with "#global_permissions"
+ When I check "Global Role"
+ Then I should see block with "#global_permissions"
+ And I should see "Global group"
+ And I should see "Glob test"
+ And I should not see "Issues can be assigned to this role"
+ When I fill in "Name" with "Manager"
+ And I click on "Create"
+ Then I should see "Successful creation."
+
+ Scenario: Global Roles can not be assigned issues to
+ Given there is a global role "global_role_x"
+ And I am already admin
+ When I go to the edit page of the role called "global_role_x"
+ Then I should not see "Issues can be assigned to this role"
diff --git a/vendored-plugins/openproject-global_roles/features/member_roles.feature b/vendored-plugins/openproject-global_roles/features/member_roles.feature
new file mode 100644
index 0000000000..cb27498a4b
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/member_roles.feature
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Unchanged Member Roles
+
+ @javascript
+ Scenario: Global Roles should not be displayed as assignable project roles
+ Given there is 1 project with the following:
+ | Name | projectname |
+ | Identifier | projectid |
+ And there is a global role "GlobalRole1"
+ And there is a role "MemberRole1"
+ And I am already admin
+ When I go to the new member page of the project "projectid"
+ Then I should see "MemberRole1" within "#member_role_ids"
+ Then I should not see "GlobalRole1" within "#member_role_ids"
diff --git a/vendored-plugins/openproject-global_roles/features/no_module.feature b/vendored-plugins/openproject-global_roles/features/no_module.feature
new file mode 100644
index 0000000000..fb8535711b
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/no_module.feature
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: No Module
+
+ Scenario: Global Rights Modules do not exist as Project -> Settings -> Modules
+ Given there is the global permission "glob_test" of the module "global"
+ And there is 1 project with the following:
+ | name | test |
+ | identifier | test |
+ And I am already admin
+ When I go to the members tab of the settings page for the project "test"
+ Then I should not see "Global"
diff --git a/vendored-plugins/openproject-global_roles/features/step_definitions/global_role_steps.rb b/vendored-plugins/openproject-global_roles/features/step_definitions/global_role_steps.rb
new file mode 100644
index 0000000000..0bca966d2f
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/features/step_definitions/global_role_steps.rb
@@ -0,0 +1,93 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Given /^there is the global permission "(.+)?" of the module "(.+)?"$/ do |perm_name, perm_module|
+ Redmine::AccessControl.map do |map|
+ map.project_module perm_module.to_sym do |mod|
+ mod.permission perm_name.to_sym, { dont: :care }, project_module: perm_module.to_sym, global: true
+ end
+ end
+end
+
+Given /^the global permission "(.+)?" of the module "(.+)?" is defined$/ do |perm_name, perm_module|
+ as_admin do
+ permissions = Redmine::AccessControl.modules_permissions(perm_module)
+ permissions.detect { |p| p.name == perm_name.to_sym && p.global? }.should_not be_nil
+ end
+end
+
+Given /^there is a global [rR]ole "([^\"]*)"$/ do |name|
+ FactoryGirl.create(:global_role, name: name) unless GlobalRole.find_by_name(name)
+end
+
+Given /^the global [rR]ole "([^\"]*)" may have the following [rR]ights:$/ do |role, table|
+ r = GlobalRole.find_by_name(role)
+ fail "No such role was defined: #{role}" unless r
+ as_admin do
+ available_perms = Redmine::AccessControl.permissions.collect(&:name)
+ r.permissions = []
+
+ table.raw.each do |perm|
+ permission = perm.first
+ unless permission.blank?
+ permission = permission.tr(' ', '_').underscore.to_sym
+ if available_perms.include?(:"#{permission}")
+ r.permissions << permission
+ end
+ end
+ end
+
+ r.save!
+ end
+end
+
+Given /^the [Uu]ser (.+) has the global role (.+)$/ do |user, role|
+ user = User.find_by_login(user.delete("\""))
+ role = GlobalRole.find_by_name(role.delete("\""))
+
+ as_admin do
+ FactoryGirl.create(:principal_role, principal: user, role: role)
+ end
+end
+
+When /^I select the available global role (.+)$/ do |role|
+ r = GlobalRole.find_by_name(role.delete("\""))
+ fail "No such role was defined: #{role}" unless r
+ steps %(
+ When I check "principal_role_role_ids_#{r.id}"
+ )
+end
+
+When /^I delete the assigned role (.+)$/ do |role|
+ g = GlobalRole.find_by_name(role.delete("\""))
+ fail "No such role was defined: #{role}" unless g
+ fail 'More than one or no principal has this role' if g.principal_roles.length != 1
+
+ steps %(
+ When I follow "Delete" within "#principal_role-#{g.principal_roles[0].id}"
+ )
+end
+
+Then /^I should (not )?see block with "(.+)?"$/ do |negative, id|
+ unless negative
+ expect(page).to have_css("#{id}", visible: true)
+ else
+ expect(page).to have_css("#{id}", visible: false)
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles.rb
new file mode 100644
index 0000000000..453475e872
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module GlobalRoles
+ require 'open_project/global_roles/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/engine.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/engine.rb
new file mode 100644
index 0000000000..1812ecc914
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/engine.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/plugins'
+
+module OpenProject::GlobalRoles
+ class Engine < ::Rails::Engine
+ engine_name :openproject_global_roles
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-global_roles',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0'
+
+ assets %w(global_roles/global_roles.js)
+
+ patches [:Principal, :Role, :User, :RolesController, :UsersController, :RolesHelper, :UsersHelper]
+
+
+ global_roles_attributes = [:id, :principal_id, :role_id, role_ids: []]
+ additional_permitted_attributes global_roles_principal_role: global_roles_attributes
+
+ initializer 'global_roles.patch_access_control' do
+ require 'open_project/global_roles/patches/access_control_patch'
+ require 'open_project/global_roles/patches/permission_patch'
+ end
+
+ initializer 'global_roles.register_global_permission' do
+ Redmine::AccessControl.permission(:add_project).global = true
+ end
+
+ config.to_prepare do
+ User.register_allowance_evaluator OpenProject::GlobalRoles::PrincipalAllowanceEvaluator::Global
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches.rb
new file mode 100644
index 0000000000..6dc6bcd6b3
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/access_control_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/access_control_patch.rb
new file mode 100644
index 0000000000..dd69b2dc71
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/access_control_patch.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+ module AccessControlPatch
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ base.class_eval do
+ class << self
+ unless method_defined?(:available_project_modules_without_no_global)
+ alias_method :available_project_modules_without_no_global, :available_project_modules
+ end
+ alias_method :available_project_modules, :available_project_modules_with_no_global
+ end
+ end
+ end
+
+ module ClassMethods
+ def available_project_modules_with_no_global
+ @available_project_modules = (
+ @permissions.reject(&:global?).collect(&:project_module) +
+ @project_modules_without_permissions
+ ).uniq.compact
+ available_project_modules_without_no_global
+ end
+
+ def global_permissions
+ @permissions.select(&:global?)
+ end
+ end
+ end
+end
+
+Redmine::AccessControl.send(:include, OpenProject::GlobalRoles::Patches::AccessControlPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/permission_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/permission_patch.rb
new file mode 100644
index 0000000000..9acb0e479f
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/permission_patch.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'redmine/access_control'
+
+module OpenProject::GlobalRoles::Patches
+ module PermissionPatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ alias_method_chain :initialize, :global_option
+ end
+ end
+
+ module InstanceMethods
+ def initialize_with_global_option(name, hash, options)
+ @global = options[:global] || false
+ initialize_without_global_option(name, hash, options)
+ end
+
+ def global?
+ @global || global_require
+ end
+
+ def global=(bool)
+ @global = bool
+ end
+
+ private
+
+ def global_require
+ @require && @require == :global
+ end
+ end
+ end
+end
+
+Redmine::AccessControl::Permission.send(:include, OpenProject::GlobalRoles::Patches::PermissionPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/principal_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/principal_patch.rb
new file mode 100644
index 0000000000..b0d843a036
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/principal_patch.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+ module PrincipalPatch
+ def self.included(base)
+ base.class_eval do
+ has_many :principal_roles, dependent: :destroy
+ has_many :global_roles, through: :principal_roles, source: :role
+ end
+ end
+ end
+end
+
+Principal.send(:include, OpenProject::GlobalRoles::Patches::PrincipalPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/role_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/role_patch.rb
new file mode 100644
index 0000000000..ed465e082d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/role_patch.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+ module RolePatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+ base.send(:extend, ClassMethods)
+
+ base.class_eval do
+ scope :givable, lambda {
+ where(builtin: 0, type: 'Role').order('position')
+ }
+
+ class << self
+ unless method_defined?(:find_all_givable_without_no_global_roles)
+ alias_method :find_all_givable_without_no_global_roles, :find_all_givable
+ end
+ alias_method :find_all_givable, :find_all_givable_with_no_global_roles
+ end
+
+ alias_method_chain :setable_permissions, :no_global_roles
+ end
+ end
+
+ module ClassMethods
+ def find_all_givable_with_no_global_roles
+ givable
+ end
+ end
+
+ module InstanceMethods
+ def setable_permissions_with_no_global_roles
+ setable_permissions = setable_permissions_without_no_global_roles
+ setable_permissions -= Redmine::AccessControl.global_permissions
+ setable_permissions
+ end
+ end
+ end
+end
+
+Role.send(:include, OpenProject::GlobalRoles::Patches::RolePatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_controller_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_controller_patch.rb
new file mode 100644
index 0000000000..d35218f498
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_controller_patch.rb
@@ -0,0 +1,69 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+ module RolesControllerPatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ alias_method_chain :create, :global_roles
+ alias_method_chain :new, :global_roles
+ end
+ end
+
+ module InstanceMethods
+ def new_with_global_roles
+ new_without_global_roles
+
+ @member_permissions = (@role.setable_permissions || @permissions)
+ @global_permissions = GlobalRole.setable_permissions
+ end
+
+ def create_with_global_roles
+ if params['global_role']
+ create_global_role
+ else
+ # we have to duplicate unpatched behaviour here in order to set the parameters for the overwritten views
+ @role = Role.new(permitted_params.role? || { permissions: Role.non_member.permissions })
+ @member_permissions = (@role.setable_permissions || @permissions)
+ @global_permissions = GlobalRole.setable_permissions
+ create_without_global_roles
+ end
+ end
+
+ private
+
+ def create_global_role
+ @role = GlobalRole.new permitted_params.role
+ if @role.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to action: 'index'
+ else
+ @roles = Role.all order: 'builtin, position'
+ @member_permissions = Role.new.setable_permissions
+ @global_permissions = GlobalRole.setable_permissions
+ render template: 'roles/new'
+ end
+ end
+ end
+ end
+end
+
+RolesController.send(:include, OpenProject::GlobalRoles::Patches::RolesControllerPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_helper_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_helper_patch.rb
new file mode 100644
index 0000000000..3f0e55329c
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/roles_helper_patch.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'roles_helper'
+
+module OpenProject::GlobalRoles::Patches
+ module RolesHelperPatch
+ def self.included(base)
+ base.class_eval do
+ def permissions_id(permissions)
+ 'permissions_' + permissions[0].hash.to_s
+ end
+ end
+ end
+ end
+end
+
+RolesHelper.send(:include, OpenProject::GlobalRoles::Patches::RolesHelperPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/user_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/user_patch.rb
new file mode 100644
index 0000000000..5742a790de
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/user_patch.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::GlobalRoles::Patches
+ module UserPatch
+ def self.included(base)
+ base.class_eval do
+ has_many :principal_roles, dependent: :destroy, foreign_key: 'principal_id'
+ has_many :global_roles, through: :principal_roles, source: :role
+ end
+ end
+ end
+end
+
+User.send(:include, OpenProject::GlobalRoles::Patches::UserPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_controller_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_controller_patch.rb
new file mode 100644
index 0000000000..11aae85760
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_controller_patch.rb
@@ -0,0 +1,42 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'users_controller'
+
+module OpenProject::GlobalRoles::Patches
+ module UsersControllerPatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ before_filter :add_global_roles, only: [:edit]
+ end
+ end
+
+ module InstanceMethods
+ private
+
+ def add_global_roles
+ @global_roles = GlobalRole.all
+ end
+ end
+ end
+end
+
+UsersController.send(:include, OpenProject::GlobalRoles::Patches::UsersControllerPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_helper_patch.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_helper_patch.rb
new file mode 100644
index 0000000000..e09d88c819
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/patches/users_helper_patch.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'users_helper'
+
+module OpenProject::GlobalRoles::Patches
+ module UsersHelperPatch
+ def self.included(base)
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+ alias_method_chain :user_settings_tabs, :global_roles
+ end
+ end
+
+ module InstanceMethods
+ def user_settings_tabs_with_global_roles
+ tabs = user_settings_tabs_without_global_roles
+ @global_roles ||= GlobalRole.all
+ tabs << { name: 'global_roles', partial: 'users/global_roles', label: 'global_roles' }
+ tabs
+ end
+ end
+ end
+end
+
+UsersHelper.send(:include, OpenProject::GlobalRoles::Patches::UsersHelperPatch)
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/principal_allowance_evaluator/global.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/principal_allowance_evaluator/global.rb
new file mode 100644
index 0000000000..6656dfc0db
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/principal_allowance_evaluator/global.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module GlobalRoles
+ module PrincipalAllowanceEvaluator
+ class Global < OpenProject::PrincipalAllowanceEvaluator::Base
+ def granted_for_global?(membership, action, options)
+ return false unless membership.is_a?(PrincipalRole)
+ granted = super
+
+ granted || membership.role.allowed_to?(action).present?
+ end
+
+ def global_granting_candidates
+ @user.principal_roles
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/version.rb b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/version.rb
new file mode 100644
index 0000000000..b6c004d117
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/open_project/global_roles/version.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module GlobalRoles
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/lib/openproject-global_roles.rb b/vendored-plugins/openproject-global_roles/lib/openproject-global_roles.rb
new file mode 100644
index 0000000000..2c5816577d
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/lib/openproject-global_roles.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/global_roles'
diff --git a/vendored-plugins/openproject-global_roles/openproject-global_roles.gemspec b/vendored-plugins/openproject-global_roles/openproject-global_roles.gemspec
new file mode 100644
index 0000000000..78a0012068
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/openproject-global_roles.gemspec
@@ -0,0 +1,25 @@
+$LOAD_PATH.push File.expand_path('../lib', __FILE__)
+
+# Maintain your gem's version:
+require 'open_project/global_roles/version'
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = 'openproject-global_roles'
+ s.version = OpenProject::GlobalRoles::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/plugin-global-roles'
+ s.summary = 'OpenProject Global Roles'
+ s.description = "Adds global roles not bound to a project. A user can have a global role allowing to
+ perform actions outside of the scope of a specific project normally only allowed for administrators.
+ By assigning the permission to create projects to a global role, non-administrators can create top-level projects."
+ s.license = 'GPLv3'
+
+ s.files = Dir['{app,config,db,lib,doc}/**/*', 'README.md']
+ s.test_files = Dir['spec/**/*']
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_development_dependency 'factory_girl_rails', '~> 4.0'
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/controllers/principal_roles_controller_spec.rb b/vendored-plugins/openproject-global_roles/spec/controllers/principal_roles_controller_spec.rb
new file mode 100644
index 0000000000..ef6b157792
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/controllers/principal_roles_controller_spec.rb
@@ -0,0 +1,191 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe PrincipalRolesController, type: :controller do
+ before(:each) do
+ allow(@controller).to receive(:require_admin).and_return(true)
+ allow(@controller).to receive(:check_if_login_required).and_return(true)
+ allow(@controller).to receive(:set_localization).and_return(true)
+
+ @principal_role = mock_model PrincipalRole
+
+ if privacy_plugin_loaded?
+ allow(@principal_role).to receive(:privacy_unnecessary=)
+ allow(@principal_role).to receive(:valid?).and_return(true)
+ allow(@principal_role).to receive(:privacy_statement_necessary?).and_return(false)
+ end
+
+ allow(@principal_role).to receive(:id).and_return(23)
+ allow(PrincipalRole).to receive(:find).and_return @principal_role
+ disable_flash_sweep
+ disable_log_requesting_user
+ end
+
+ describe '#post' do
+ before :each do
+ @params = { 'principal_role' => { 'principal_id' => '3', 'role_ids' => ['7'] } }
+ end
+
+ unless privacy_plugin_loaded? # tests than are defined in privacy_plugin
+
+ describe '#create' do
+ before :each do
+ end
+
+ describe 'SUCCESS' do
+ before :each do
+ @global_role = mock_model(GlobalRole)
+ allow(@global_role).to receive(:id).and_return(42)
+ ##
+ # Note this test uses doubles which may break depending on the loaded plugins.
+ # Specifically extra stubs have been added for these tests to work with the
+ # openproject-impermanent_memberships plugin which would be otherwise unexpected.
+ # Those stubs are marked with the comment "only necessary with impermanent-memberships".
+ #
+ # If this problem occurs again with another plugin (or the same, really) this should be fixed for good
+ # by using FactoryGirl to create actual model instances.
+ # I'm only patching this up right now because I don't want to spend any more time on it and
+ # the added methods are orthogonal to the test, also additional, unused stubs won't break things
+ # as opposed to missing ones.
+ #
+ # And yet: @TODO Don't use doubles but FactoryGirl.
+ allow(@global_role).to receive(:id).and_return(42)
+ allow(@global_role).to receive(:permanent?).and_return(false) # only necessary with impermanent-memberships
+ allow(Role).to receive(:find).and_return([@global_role])
+ allow(PrincipalRole).to receive(:new).and_return(@principal_role)
+ @user = mock_model User
+ allow(@user).to receive(:valid?).and_return(true)
+ allow(@user).to receive(:logged?).and_return(true)
+ allow(@user).to receive(:global_roles).and_return([]) # only necessary with impermanent-memberships
+ allow(Principal).to receive(:find).and_return(@user)
+ allow(@principal_role).to receive(:role=)
+ allow(@principal_role).to receive(:role).and_return(@global_role)
+ allow(@principal_role).to receive(:principal_id=)
+ allow(@principal_role).to receive(:save)
+ allow(@principal_role).to receive(:role_id).and_return(@global_role.id)
+ allow(@principal_role).to receive(:valid?).and_return(true)
+ end
+
+ describe 'js' do
+ before :each do
+ response_should_render :replace,
+ 'available_principal_roles',
+ partial: 'users/available_global_roles',
+ locals: { global_roles: anything,
+ user: anything }
+ response_should_render :insert_html,
+ :top, 'table_principal_roles_body',
+ partial: 'principal_roles/show_table_row',
+ locals: { principal_role: anything }
+
+ # post :create, { "format" => "js", "principal_role"=>{"principal_id"=>"3", "role_ids"=>["7"]}}
+ xhr :post, :create, @params
+ end
+
+ it { expect(response).to be_success }
+ end
+ end
+ end
+ end
+ end
+
+ describe '#put' do
+ before :each do
+ @params = { 'principal_role' => { 'id' => '6', 'role_id' => '5' } }
+ end
+
+ describe '#update' do
+ before(:each) do
+ allow(@principal_role).to receive(:update_attributes)
+ end
+
+ describe 'SUCCESS' do
+ describe 'js' do
+ before :each do
+ allow(@principal_role).to receive(:valid?).and_return(true)
+
+ response_should_render :replace,
+ "principal_role-#{@principal_role.id}",
+ partial: 'principal_roles/show_table_row',
+ locals: { principal_role: anything }
+
+ xhr :put, :update, @params
+ end
+
+ it { expect(response).to be_success }
+ end
+ end
+
+ describe 'FAILURE' do
+ describe 'js' do
+ before :each do
+ allow(@principal_role).to receive(:valid?).and_return(false)
+ response_should_render :insert_html,
+ :top,
+ 'tab-content-global_roles',
+ partial: 'errors'
+
+ xhr :put, :update, @params
+ end
+
+ it { expect(response).to be_success }
+ end
+ end
+ end
+ end
+
+ describe '#delete' do
+ before :each do
+ allow(@principal_role).to receive(:principal_id).and_return(1)
+ @user = mock_model User
+ allow(@user).to receive(:logged?).and_return(true)
+ # only necessary with impermanent-memberships
+ allow(@user).to receive(:global_roles).and_return([])
+ allow(Principal).to receive(:find).and_return(@user)
+ allow(@principal_role).to receive(:destroy)
+
+ # only necessary with impermanent-memberships
+ allow(@principal_role).to receive(:role).and_return(Struct.new(:id, :permanent?).new(42, false))
+ @params = { 'id' => '1' }
+ end
+
+ describe '#destroy' do
+ describe 'SUCCESS' do
+ before :each do
+ response_should_render :remove, "principal_role-#{@principal_role.id}"
+ response_should_render :replace,
+ 'available_principal_roles',
+ partial: 'users/available_global_roles',
+ locals: { global_roles: anything,
+ user: anything }
+ end
+
+ describe 'js' do
+ before :each do
+ xhr :delete, :destroy, @params
+ end
+
+ it { expect(response).to be_success }
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/controllers/roles_controller_spec.rb b/vendored-plugins/openproject-global_roles/spec/controllers/roles_controller_spec.rb
new file mode 100644
index 0000000000..ea6cd12f3c
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/controllers/roles_controller_spec.rb
@@ -0,0 +1,316 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe RolesController, type: :controller do
+ before do
+ allow(@controller).to receive(:check_if_login_required)
+ expect(@controller).to receive(:require_admin)
+ disable_flash_sweep
+ end
+
+ after do
+ User.current = nil
+ end
+
+ shared_examples_for 'index' do
+ it { expect(response).to be_success }
+ it { expect(assigns(:roles)).to eql(@roles) }
+ it { expect(response).to render_template 'roles/index' }
+ end
+
+ shared_examples_for 'global assigns' do
+ it { expect(assigns(:global_permissions)).to eql @global_role.setable_permissions }
+ it { expect(assigns(:global_roles)).to eql @global_roles }
+ it { expect(assigns(:global_role)).to eql @global_role }
+ end
+
+ shared_examples_for 'permission assigns' do
+ it { expect(assigns(:member_permissions)).to eql @member_role.setable_permissions }
+ it { expect(assigns(:global_permissions)).to eql GlobalRole.setable_permissions }
+ end
+
+ shared_examples_for 'successful create' do
+ it { expect(response).to be_redirect }
+ it { expect(response).to redirect_to '/admin/roles' }
+ it { expect(flash[:notice]).to eql I18n.t(:notice_successful_create) }
+ end
+
+ shared_examples_for 'failed create' do
+ it { expect(response).to be_success }
+ it { expect(response).to render_template 'new' }
+ end
+
+ describe 'WITH get' do
+ describe 'VERB', :index do
+ before do
+ mock_role_find
+ end
+
+ describe 'html' do
+ before { get 'index' }
+
+ it_should_behave_like 'index'
+ end
+
+ describe 'xhr' do
+ before { xhr :get, 'index' }
+
+ it_should_behave_like 'index'
+ end
+ end
+
+ describe 'VERB', :new do
+ before do
+ @member_role = mocks_for_creating Role
+ allow(GlobalRole).to receive(:setable_permissions).and_return([:perm1, :perm2, :perm3])
+ @non_member_role = mock_model Role
+ mock_permissions_on @non_member_role
+ allow(Role).to receive(:non_member).and_return(@non_member_role)
+
+ mock_role_find
+ get 'new'
+ end
+
+ it { expect(response).to be_success }
+ it { expect(response).to render_template 'roles/new' }
+ it { expect(assigns(:member_permissions)).to eql @member_role.setable_permissions }
+ it { expect(assigns(:roles)).to eql @roles }
+ it { expect(assigns(:role)).to eql @member_role }
+ it { expect(assigns(:global_permissions)).to eql GlobalRole.setable_permissions }
+ end
+
+ describe 'VERB', :edit do
+ before(:each) do
+ @member_role = mocks_for_creating Role
+ @global_role = mocks_for_creating GlobalRole
+ mock_role_find
+ end
+
+ describe 'WITH member_role id' do
+ before do
+ @params = { 'id' => '1' }
+ allow(Role).to receive(:find).and_return(@member_role)
+ end
+
+ describe 'RESULT' do
+ describe 'success' do
+ describe 'html' do
+ before { get :edit, @params }
+
+ it { expect(response).to be_success }
+ it { expect(response).to render_template 'roles/edit' }
+ it { expect(assigns(:role)).to eql @member_role }
+ it { expect(assigns(:permissions)).to eql @member_role.setable_permissions }
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'WITH post' do
+ before(:each) do
+ @member_role = mocks_for_creating Role
+ @global_role = mocks_for_creating GlobalRole
+ mock_role_find
+ allow(Role).to receive(:find).with('1').and_return(@member_role)
+ allow(Role).to receive(:find).with('2').and_return(@global_role)
+ end
+
+ describe 'VERB', :create do
+ describe 'WITH member_role params' do
+ before do
+ @params = { 'role' => { 'name' => 'role',
+ 'permissions' => %w(perm1 perm2 perm3),
+ 'assignable' => '1' } }
+ end
+
+ describe 'RESULT' do
+ describe 'success' do
+ before(:each) do
+ expect(Role).to receive(:new).with(@params['role']).and_return(@member_role)
+ allow(@member_role).to receive(:save).and_return(true)
+ allow(@member_role).to receive(:errors).and_return([])
+ end
+
+ describe 'html' do
+ before do
+ post 'create', @params
+ end
+
+ it_should_behave_like 'successful create'
+ it { expect(assigns(:role)).to eql @member_role }
+ end
+ end
+
+ describe 'failure' do
+ before(:each) do
+ expect(Role).to receive(:new).with(@params['role']).and_return(@member_role)
+ allow(@member_role).to receive(:save).and_return(false)
+ allow(@member_role).to receive(:errors).and_return(['something is wrong'])
+ end
+
+ describe 'html' do
+ before { post 'create', @params }
+
+ it_should_behave_like 'failed create'
+ it { expect(assigns(:role)).to eql @member_role }
+ it { expect(assigns(:roles)).to eql Role.all }
+ it_should_behave_like 'permission assigns'
+ end
+ end
+ end
+ end
+
+ describe 'WITH global_role params' do
+ before do
+ @params = { 'role' => { 'name' => 'role',
+ 'permissions' => %w(perm1 perm2 perm3)
+ },
+ 'global_role' => '1' }
+ end
+
+ describe 'RESULTS' do
+ describe 'success' do
+ before(:each) do
+ expect(GlobalRole).to receive(:new).with(@params['role']).and_return(@global_role)
+ allow(@global_role).to receive(:save).and_return(true)
+ end
+
+ describe 'html' do
+ before { post 'create', @params }
+
+ it_should_behave_like 'successful create'
+ end
+ end
+
+ describe 'failure' do
+ before(:each) do
+ expect(GlobalRole).to receive(:new).with(@params['role']).and_return(@global_role)
+ allow(@global_role).to receive(:save).and_return(false)
+ end
+
+ describe 'html' do
+ before { post 'create', @params }
+
+ it_should_behave_like 'failed create'
+ it { expect(assigns(:role)).to eql @global_role }
+ it { expect(assigns(:roles)).to eql Role.all }
+ it_should_behave_like 'permission assigns'
+ end
+ end
+ end
+ end
+ end
+
+ describe 'VERB', :destroy do
+ shared_examples_for 'destroy results' do
+ describe 'success' do
+ before do
+ expect(@role).to receive(:destroy)
+ post 'destroy', @params
+ end
+
+ it { expect(response).to be_redirect }
+ it { expect(response).to redirect_to '/admin/roles' }
+ end
+ end
+
+ describe 'WITH member_role params' do
+ before do
+ @params = { 'class' => 'Role', 'id' => '1' }
+ @role = @member_role
+ end
+
+ describe 'RESULTS' do
+ it_should_behave_like 'destroy results'
+ end
+ end
+
+ describe 'WITH global_role params' do
+ before do
+ @params = { 'class' => 'Role', 'id' => '2' }
+ @role = @global_role
+ end
+
+ describe 'RESULTS' do
+ it_should_behave_like 'destroy results'
+ end
+ end
+ end
+
+ describe 'VERB', :update do
+ shared_examples_for 'update results' do
+ describe 'success' do
+ describe 'html' do
+ before do
+ expect(@role).to receive(:update_attributes).with(@params['role']).and_return(true)
+ allow(@role).to receive(:errors).and_return([])
+ post :update, @params
+ end
+
+ it { expect(response).to be_redirect }
+ it { expect(response).to redirect_to '/admin/roles' }
+ it { expect(flash[:notice]).to eql I18n.t(:notice_successful_update) }
+ end
+ end
+
+ describe 'failure' do
+ describe 'html' do
+ before(:each) do
+ expect(@role).to receive(:update_attributes).with(@params['role']).and_return(false)
+ allow(@role).to receive(:errors).and_return(['something is wrong'])
+ post :update, @params
+ end
+
+ it { expect(response).to render_template 'roles/edit' }
+ end
+ end
+ end
+
+ describe 'WITH member_role params' do
+ before do
+ @params = { 'role' => { 'permissions' => %w(permA permB),
+ 'name' => 'schmu' },
+ 'id' => '1' }
+ @role = @member_role
+ end
+
+ describe 'RESULT' do
+ it_should_behave_like 'update results'
+ end
+ end
+
+ describe 'WITH global_role params' do
+ before do
+ @params = { 'role' => { 'permissions' => %w(permA permB),
+ 'name' => 'schmu' },
+ 'id' => '2' }
+ @role = @global_role
+ end
+
+ describe 'RESULT' do
+ it_should_behave_like 'update results'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/controllers/users_controller_spec.rb b/vendored-plugins/openproject-global_roles/spec/controllers/users_controller_spec.rb
new file mode 100644
index 0000000000..385f0cf25a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/controllers/users_controller_spec.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe UsersController, type: :controller do
+ before(:each) do
+ allow(@controller).to receive(:require_admin).and_return(true)
+ allow(@controller).to receive(:check_if_login_required)
+ allow(@controller).to receive(:set_localization)
+ @global_roles = [mock_model(GlobalRole), mock_model(GlobalRole)]
+ allow(GlobalRole).to receive(:all).and_return(@global_roles)
+ user_mock = mock_model User
+ allow(user_mock).to receive(:logged?).and_return(true)
+ allow(User).to receive(:find).with(any_args).and_return(user_mock)
+
+ disable_log_requesting_user
+ end
+
+ describe 'get' do
+ before :each do
+ @params = { 'id' => '1' }
+ end
+
+ describe '#edit' do
+ before :each do
+ end
+
+ describe 'RESULT' do
+ before :each do
+ end
+
+ describe 'html' do
+ before :each do
+ get 'edit', @params
+ end
+
+ it { expect(response).to be_success }
+ it { expect(assigns(:global_roles)).to eql @global_roles }
+ it { expect(response).to render_template 'users/edit' }
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/factories/global_role_factory.rb b/vendored-plugins/openproject-global_roles/spec/factories/global_role_factory.rb
new file mode 100644
index 0000000000..518e2898f0
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/factories/global_role_factory.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :global_role do
+ sequence(:name) { |n| "Global Role #{n}" }
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/factories/principal_role_factory.rb b/vendored-plugins/openproject-global_roles/spec/factories/principal_role_factory.rb
new file mode 100644
index 0000000000..fcea8b6e02
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/factories/principal_role_factory.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :principal_role do |pr|
+ pr.association :role, factory: :global_role
+ pr.association :principal, factory: :user
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/lib/access_control_spec.rb b/vendored-plugins/openproject-global_roles/spec/lib/access_control_spec.rb
new file mode 100644
index 0000000000..a2144237cd
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/lib/access_control_spec.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Redmine::AccessControl do
+ before(:each) do
+ stash_access_control_permissions
+
+ Redmine::AccessControl.map do |map|
+ map.permission :proj0, { dont: :care }, require: :member
+ map.permission :global0, { dont: :care }, global: true
+ map.permission :proj1, { dont: :care }
+
+ map.project_module :global_module do |mod|
+ mod.permission :global1, { dont: :care }, global: true
+ end
+
+ map.project_module :project_module do |mod|
+ mod.permission :proj2, { dont: :care }
+ end
+
+ map.project_module :mixed_module do |mod|
+ mod.permission :proj3, { dont: :care }
+ mod.permission :global2, { dont: :care }, global: true
+ end
+ end
+ end
+
+ after(:each) do
+ restore_access_control_permissions
+ end
+
+ describe 'class methods' do
+ describe '#global_permissions' do
+ it { expect(Redmine::AccessControl.global_permissions.size).to eq(3) }
+ it { expect(Redmine::AccessControl.global_permissions.collect(&:name)).to include(:global0) }
+ it { expect(Redmine::AccessControl.global_permissions.collect(&:name)).to include(:global1) }
+ it { expect(Redmine::AccessControl.global_permissions.collect(&:name)).to include(:global2) }
+ end
+
+ describe '#available_project_modules' do
+ it { expect(Redmine::AccessControl.available_project_modules.include?(:global_module)).to be_falsey }
+ it { expect(Redmine::AccessControl.available_project_modules.include?(:global_module)).to be_falsey }
+ it { expect(Redmine::AccessControl.available_project_modules.include?(:mixed_module)).to be_truthy }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/lib/global_roles/principal_allowance_evaluator/global_spec.rb b/vendored-plugins/openproject-global_roles/spec/lib/global_roles/principal_allowance_evaluator/global_spec.rb
new file mode 100644
index 0000000000..931268ae0e
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/lib/global_roles/principal_allowance_evaluator/global_spec.rb
@@ -0,0 +1,92 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+describe OpenProject::GlobalRoles::PrincipalAllowanceEvaluator::Global do
+ let(:klass) { OpenProject::GlobalRoles::PrincipalAllowanceEvaluator::Global }
+ let(:user) { FactoryGirl.build(:user) }
+ let(:filter) { klass.new user }
+ let(:member) { FactoryGirl.build(:member) }
+ let(:principal_role) {
+ FactoryGirl.build(:principal_role,
+ role: role)
+ }
+ let(:principal_role2) { FactoryGirl.build(:principal_role) }
+ let(:role) { FactoryGirl.build(:global_role) }
+ let(:project) { FactoryGirl.build(:project) }
+
+ describe '#granted_for_project?' do
+ it { expect(filter.granted_for_project?(member, :action, project)).to be_falsey }
+ end
+
+ describe '#denied_for_project?' do
+ it { expect(filter.denied_for_project?(member, :action, project)).to be_falsey }
+ end
+
+ describe '#granted_for_global?' do
+ describe 'WHEN checking a Member' do
+ it { expect(filter.granted_for_global?(member, :action, {})).to be_falsey }
+ end
+
+ describe "WHEN checking a PrincipalRole
+ WHEN the PrincipalRole has a Role that is allowed the action" do
+ before do
+ role.permissions = [:action]
+ end
+
+ it { expect(filter.granted_for_global?(principal_role, :action, {})).to be_truthy }
+ end
+
+ describe "WHEN checking a PrincipalRole
+ WHEN the PrincipalRole has a Role that is not allowed the action" do
+ it { expect(filter.granted_for_global?(principal_role, :action, {})).to be_falsey }
+ end
+ end
+
+ describe '#denied_for_global?' do
+ it { expect(filter.denied_for_global?(principal_role, :action, {})).to be_falsey }
+ end
+
+ describe '#project_granting_candidates' do
+ it { expect(filter.project_granting_candidates(project)).to match_array([]) }
+ end
+
+ describe '#global_granting_candidates' do
+ describe 'WHEN the user has a PrincipalRole assigned' do
+ before do
+ user.principal_roles = [principal_role]
+ end
+
+ it { filter.global_granting_candidates =~ [principal_role] }
+ end
+
+ describe 'WHEN the user has multiple PrincipalRole assigned' do
+ before do
+ user.principal_roles = [principal_role, principal_role2]
+ end
+
+ it { filter.global_granting_candidates =~ [principal_role, principal_role2] }
+ end
+
+ describe 'WHEN the user has no PrincipalRoles assigned' do
+ it { filter.global_granting_candidates =~ [] }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/lib/loader_spec.rb b/vendored-plugins/openproject-global_roles/spec/lib/loader_spec.rb
new file mode 100644
index 0000000000..7a0799c376
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/lib/loader_spec.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe 'Seeding' do
+ describe '#load' do
+ before :each do
+ stash_access_control_permissions
+ create_non_member_role
+ create_anonymous_role
+ end
+
+ after(:each) do
+ restore_access_control_permissions
+ end
+
+ it 'expects all generated roles to have the type \'Role\'' do
+ expect(Role.pluck(:type).uniq).to match_array ['Role']
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/lib/permission_spec.rb b/vendored-plugins/openproject-global_roles/spec/lib/permission_spec.rb
new file mode 100644
index 0000000000..28cd3e2d97
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/lib/permission_spec.rb
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Redmine::AccessControl::Permission do
+ describe 'WHEN setting global permission' do
+ describe 'creating with', :new do
+ before { @permission = Redmine::AccessControl::Permission.new(:perm, { cont: [:action] }, { global: true }) }
+ describe '#global?' do
+ it { expect(@permission.global?).to be_truthy }
+ end
+ end
+ end
+
+ describe 'setting non_global' do
+ describe 'creating with', :new do
+ before { @permission = Redmine::AccessControl::Permission.new :perm, { cont: [:action] }, { global: false } }
+
+ describe '#global?' do
+ it { expect(@permission.global?).to be_falsey }
+ end
+ end
+
+ describe 'creating with', :new do
+ before { @permission = Redmine::AccessControl::Permission.new :perm, { cont: [:action] }, {} }
+
+ describe '#global?' do
+ it { expect(@permission.global?).to be_falsey }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/models/global_role_spec.rb b/vendored-plugins/openproject-global_roles/spec/models/global_role_spec.rb
new file mode 100644
index 0000000000..46afb60562
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/models/global_role_spec.rb
@@ -0,0 +1,184 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe GlobalRole, type: :model do
+ before { GlobalRole.create name: 'globalrole', permissions: ['permissions'] }
+
+ it { is_expected.to have_many :principals }
+ it { is_expected.to have_many :principal_roles }
+ it { is_expected.to validate_presence_of :name }
+ it { is_expected.to validate_uniqueness_of :name }
+ it { is_expected.to validate_length_of(:name).is_at_most(30) }
+
+ describe 'attributes' do
+ before { @role = GlobalRole.new }
+
+ subject { @role }
+
+ it { is_expected.to respond_to :name }
+ it { is_expected.to respond_to :permissions }
+ it { is_expected.to respond_to :position }
+ end
+
+ describe 'class methods' do
+ describe 'WITH available global permissions defined' do
+ before do
+ @permission_options = [:perm1, :perm2, :perm3]
+ allow(Redmine::AccessControl).to receive(:global_permissions).and_return(@permission_options)
+ end
+
+ describe '#setable_permissions' do
+ it { expect(GlobalRole.setable_permissions).to eql @permission_options }
+ end
+ end
+ end
+
+ describe 'instance methods' do
+ before do
+ @role = GlobalRole.new
+
+ if costs_plugin_loaded?
+ @perm = Object.new
+ allow(Redmine::AccessControl).to receive(:permission).and_return @perm
+ allow(@perm).to receive(:inherited_by).and_return([])
+ allow(@perm).to receive(:name).and_return(:perm)
+ allow(@perm).to receive(:inherits).and_return([])
+ end
+ end
+
+ describe 'WITH no attributes set' do
+ before do
+ @role = GlobalRole.new
+ end
+
+ describe '#permissions' do
+ subject { @role.permissions }
+
+ it { is_expected.to be_an_instance_of(Array) }
+ it 'has no items' do
+ expect(subject.size).to eq(0)
+ end
+ end
+
+ describe '#permissions=' do
+ describe 'WITH parameter' do
+ before { expect(@role).to receive(:write_attribute).with(:permissions, [:perm1, :perm2]) }
+
+ it 'should write permissions' do
+ @role.permissions = [:perm1, :perm2]
+ end
+
+ it 'should write permissions only once' do
+ @role.permissions = [:perm1, :perm2, :perm2]
+ end
+
+ it 'should write permissions as symbols' do
+ @role.permissions = %w(perm1 perm2)
+ end
+
+ it 'should remove empty perms' do
+ @role.permissions = [:perm1, :perm2, '', nil]
+ end
+ end
+
+ describe 'WITHOUT parameter' do
+ before { expect(@role).to receive(:write_attribute).with(:permissions, nil) }
+
+ it 'should write permissions' do
+ @role.permissions = nil
+ end
+ end
+ end
+
+ describe '#has_permission?' do
+ it { expect(@role.has_permission?(:perm)).to be_falsey }
+ end
+
+ describe '#allowed_to?' do
+ describe 'WITH requested permission' do
+ it { expect(@role.allowed_to?(:perm1)).to be_falsey }
+ end
+ end
+ end
+
+ describe 'WITH set permissions' do
+ before { @role = GlobalRole.new permissions: [:perm1, :perm2, :perm3] }
+
+ describe '#has_permission?' do
+ it { expect(@role.has_permission?(:perm1)).to be_truthy }
+ it { expect(@role.has_permission?('perm1')).to be_truthy }
+ it { expect(@role.has_permission?(:perm5)).to be_falsey }
+ end
+
+ describe '#allowed_to?' do
+ describe 'WITH requested permission' do
+ it { expect(@role.allowed_to?(:perm1)).to be_truthy }
+ it { expect(@role.allowed_to?(:perm5)).to be_falsey }
+ end
+ end
+ end
+
+ describe 'WITH available global permissions defined' do
+ before do
+ @role = GlobalRole.new
+ @permission_options = [:perm1, :perm2, :perm3]
+ allow(Redmine::AccessControl).to receive(:global_permissions).and_return(@permission_options)
+ end
+
+ describe '#setable_permissions' do
+ it { expect(@role.setable_permissions).to eql @permission_options }
+ end
+ end
+
+ describe 'WITH set name' do
+ before { @role = GlobalRole.new name: 'name' }
+
+ describe '#to_s' do
+ it { expect(@role.to_s).to eql('name') }
+ end
+ end
+
+ describe '#destroy' do
+ before { @role = GlobalRole.create name: 'global' }
+
+ it { @role.destroy }
+ end
+
+ describe '#assignable' do
+ it { expect(@role.assignable).to be_falsey }
+ end
+
+ describe '#assignable=' do
+ it { expect { @role.assignable = true }.to raise_error ArgumentError }
+ it { expect { @role.assignable = false }.not_to raise_error }
+ end
+
+ describe '#assignable_to?' do
+ before(:each) do
+ @role = FactoryGirl.build(:global_role)
+ @user = FactoryGirl.build(:user)
+ end
+ it 'always true global roles for now' do
+ expect(@role.assignable_to?(@user)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/models/principal_role_spec.rb b/vendored-plugins/openproject-global_roles/spec/models/principal_role_spec.rb
new file mode 100644
index 0000000000..2bddd49ce1
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/models/principal_role_spec.rb
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe PrincipalRole, type: :model do
+ describe 'ATTRIBUTES' do
+ before :each do
+ end
+
+ it { is_expected.to belong_to :principal }
+ it { is_expected.to belong_to :role }
+ end
+
+ describe '#valid?' do
+ before(:each) do
+ @principal_role = FactoryGirl.build(:principal_role)
+ end
+
+ describe 'role not assignable to user' do
+ before :each do
+ allow(@principal_role.role).to receive(:assignable_to?).and_return(false)
+ end
+
+ it { expect(@principal_role.valid?).to be_falsey }
+ it {
+ @principal_role.valid?
+ expect(@principal_role.errors[:base]).to include(I18n.t(:error_can_not_be_assigned))
+ }
+ end
+
+ describe 'role assignable to user' do
+ before(:each) do
+ allow(@principal_role.role).to receive(:assignable_to?).and_return(true)
+ end
+
+ it { expect(@principal_role.valid?).to be_truthy }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/models/principal_spec.rb b/vendored-plugins/openproject-global_roles/spec/models/principal_spec.rb
new file mode 100644
index 0000000000..58d0d7818a
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/models/principal_spec.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Principal, type: :model do
+ describe 'ATTRIBUTES' do
+ before :each do
+ end
+
+ it { is_expected.to have_many :principal_roles }
+ it { is_expected.to have_many :global_roles }
+ end
+
+ describe 'WHEN deleting a principal' do
+ let(:principal) { FactoryGirl.build(:user) }
+ let(:role) { FactoryGirl.build(:global_role) }
+
+ before do
+ FactoryGirl.create(:principal_role, role: role,
+ principal: principal)
+ principal.destroy
+ end
+
+ it { expect(Role.find_by_id(role.id)).to eq(role) }
+ it { expect(PrincipalRole.where(id: principal.id)).to eq([]) }
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/models/role_spec.rb b/vendored-plugins/openproject-global_roles/spec/models/role_spec.rb
new file mode 100644
index 0000000000..6b1ac60899
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/models/role_spec.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe Role, type: :model do
+ describe 'class methods' do
+ describe '#givable' do
+ before do
+ # this should not be necessary once Role (in a membership) and GlobalRole have
+ # a common ancestor class, e.g. Role (a new one)
+ @mem_role1 = Role.create name: 'mem_role', permissions: []
+ @builtin_role1 = Role.new name: 'builtin_role1', permissions: []
+ @builtin_role1.builtin = 3
+ @builtin_role1.save
+ @global_role1 = GlobalRole.create name: 'global_role1', permissions: []
+ end
+
+ it { expect(Role.find_all_givable.size).to eq(1) }
+ it { expect(Role.find_all_givable[0]).to eql @mem_role1 }
+ end
+ end
+
+ describe 'instance methods' do
+ before do
+ @role = Role.new
+ end
+
+ describe '#setable_permissions' do
+ before { mock_permissions_for_setable_permissions }
+
+ it { expect(@role.setable_permissions).to eql([@perm1, @perm2]) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/models/user_spec.rb b/vendored-plugins/openproject-global_roles/spec/models/user_spec.rb
new file mode 100644
index 0000000000..2d940ad3ad
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/models/user_spec.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe User, type: :model do
+ let(:klass) { User }
+
+ describe '#registered_allowance_evaluators' do
+ it {
+ expect(klass.registered_allowance_evaluators).to include(
+ OpenProject::GlobalRoles::PrincipalAllowanceEvaluator::Global
+ )
+ }
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/plugin_spec_helper.rb b/vendored-plugins/openproject-global_roles/spec/plugin_spec_helper.rb
new file mode 100644
index 0000000000..a7a6308cf3
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/plugin_spec_helper.rb
@@ -0,0 +1,159 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module GlobalRoles
+ module PluginSpecHelper
+ def mobile_tan_plugin_loaded?
+ plugin_loaded?('openproject_mobile_otp')
+ end
+
+ def privacy_plugin_loaded?
+ plugin_loaded?('openproject_dtag_customizing')
+ end
+
+ def costs_plugin_loaded?
+ plugin_loaded?('openproject_costs')
+ end
+
+ def plugin_loaded?(name)
+ Redmine::Plugin.all.detect { |x| x.id == name.to_sym }.present?
+ end
+
+ def mocks_for_member_roles
+ @role = mock_model Role
+ allow(Role).to receive(:new).and_return(@role)
+
+ mock_permissions_on @role
+
+ mock_role_find
+
+ @non_mem = mock_model Role
+ allow(@non_mem).to receive(:permissions).and_return(@non_mem_perm)
+ allow(Role).to receive(:non_member).and_return(@non_mem)
+ @non_mem_perm = [:nm_perm1, :nm_perm2]
+ end
+
+ def mocks_for_global_roles
+ @role = mock_model GlobalRole
+ allow(GlobalRole).to receive(:new).and_return(@role)
+ mock_permissions_on @role
+ end
+
+ def mock_permissions_on(role)
+ permissions = [:perm1, :perm2, :perm3]
+ allow(role).to receive(:setable_permissions).and_return(permissions)
+ allow(role).to receive(:permissions).and_return(permissions << :perm4)
+ end
+
+ def mock_role_find
+ mock_member_role_find
+ mock_global_role_find
+ end
+
+ def mock_member_role_find
+ @role1 = mock_model Role
+ @role2 = mock_model Role
+ @global_role1 = mock_model GlobalRole
+ @global_role2 = mock_model GlobalRole
+ @roles = [@role1, @global_role2, @role2, @global_role1]
+ allow(Role).to receive(:find).and_return(@roles)
+ allow(Role).to receive(:all).and_return(@roles)
+ allow(Role).to receive(:order).and_return(@roles)
+ allow(@roles).to receive(:page).and_return(@roles)
+ allow(@roles).to receive(:per_page).and_return(@roles)
+ end
+
+ def mock_global_role_find
+ @global_role1 = mock_model GlobalRole
+ @global_role2 = mock_model GlobalRole
+ @global_roles = [@global_role1, @global_role2]
+ allow(GlobalRole).to receive(:find).and_return(@global_roles)
+ allow(GlobalRole).to receive(:all).and_return(@global_roles)
+ end
+
+ def mocks_for_creating(role_class)
+ role = mock_model role_class
+ allow(role_class).to receive(:new).and_return role
+ mock_permissions_on role
+ role
+ end
+
+ def disable_flash_sweep
+ @controller.instance_eval { allow(flash).to receive(:sweep) }
+ end
+
+ def disable_log_requesting_user
+ allow(@controller).to receive(:log_requesting_user)
+ end
+
+ def response_should_render(method, *params)
+ unless @page
+ @page ||= double('page')
+ expect(controller).to receive(:render).with(:update).and_yield(@page)
+ expect(controller).to receive(:render).with(no_args)
+ end
+
+ expect(@page).to receive(method).with(*params)
+ end
+
+ def mock_permissions_for_setable_permissions
+ @public_perm = mock_permissions(true, false)
+ @perm1 = mock_permissions(false, false)
+ @perm2 = mock_permissions(false, false)
+ @global_perm = mock_permissions(false, true)
+
+ @perms = [@public_perm, @perm1, @global_perm, @perm2]
+ allow(Redmine::AccessControl).to receive(:permissions).and_return(@perms)
+ allow(Redmine::AccessControl).to receive(:public_permissions).and_return([@public_perm])
+ allow(Redmine::AccessControl).to receive(:global_permissions).and_return([@global_perm])
+ end
+
+ def mock_permissions(is_public, is_global)
+ permission = Object.new
+ allow(permission).to receive(:public?).and_return(is_public)
+ allow(permission).to receive(:global?).and_return(is_global)
+ permission
+ end
+
+ def create_non_member_role
+ create_builtin_role 'No member', Role::BUILTIN_NON_MEMBER
+ end
+
+ def create_anonymous_role
+ create_builtin_role 'Anonymous', Role::BUILTIN_ANONYMOUS
+ end
+
+ def create_builtin_role(name, const)
+ Role.create(name: name, position: 0) do |role|
+ role.builtin = const
+ end
+ end
+
+ def stash_access_control_permissions
+ @stashed_permissions = Redmine::AccessControl.permissions.dup
+ Redmine::AccessControl.permissions.clear
+ end
+
+ def restore_access_control_permissions
+ Redmine::AccessControl.instance_variable_set(:@permissions, @stashed_permissions)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-global_roles/spec/spec_helper.rb b/vendored-plugins/openproject-global_roles/spec/spec_helper.rb
new file mode 100644
index 0000000000..366f75914e
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+require File.join(File.dirname(__FILE__), 'plugin_spec_helper')
+include OpenProject::GlobalRoles::PluginSpecHelper
diff --git a/vendored-plugins/openproject-global_roles/spec/views/users/_available_global_roles.html.erb_spec.rb b/vendored-plugins/openproject-global_roles/spec/views/users/_available_global_roles.html.erb_spec.rb
new file mode 100644
index 0000000000..0cd6f06e04
--- /dev/null
+++ b/vendored-plugins/openproject-global_roles/spec/views/users/_available_global_roles.html.erb_spec.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject Global Roles Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe 'users/_available_global_roles' do
+ let(:user) { FactoryGirl.create :user }
+ let(:global_roles) { FactoryGirl.create_list :global_role, 3 }
+
+ it 'links to the principal roles controller using a path, not a URL' do
+ render partial: 'users/available_global_roles',
+ locals: { user: user,
+ global_roles: global_roles }
+
+ expect(response.body).not_to match principal_roles_url
+ expect(response.body).to match principal_roles_path
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/.gitignore b/vendored-plugins/openproject-help_link/.gitignore
new file mode 100644
index 0000000000..1dfe31e605
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/.gitignore
@@ -0,0 +1,7 @@
+.bundle/
+log/*.log
+pkg/
+test/dummy/db/*.sqlite3
+test/dummy/log/*.log
+test/dummy/tmp/
+test/dummy/.sass-cache
diff --git a/vendored-plugins/openproject-help_link/.hound.yml b/vendored-plugins/openproject-help_link/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-help_link/.rubocop.yml b/vendored-plugins/openproject-help_link/.rubocop.yml
new file mode 100644
index 0000000000..a22df7c695
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - *.gemspec
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
\ No newline at end of file
diff --git a/vendored-plugins/openproject-help_link/README.md b/vendored-plugins/openproject-help_link/README.md
new file mode 100644
index 0000000000..9a39dca032
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/README.md
@@ -0,0 +1,71 @@
+OpenProject Help Link
+=====================
+
+This plugin allows you to change the target of the help link in your
+OpenProject. Doing so allows you to e.g. have a support project within your OpenProject.
+
+Requirements
+------------
+
+The OpenProject Help Link plugin requires the [OpenProject Core](https://github.com/opf/openproject/) in version greater or equal to *3.0.0*.
+
+Installation
+------------
+
+For OpenProject Help Link itself you need to add the following line to the `Gemfile.plugins` of OpenProject (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-help_link", git: "https://github.com/finnlabs/openproject-help_link.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+Usage
+-----
+
+The url of the help link is configurable with this plugin. You can set the url in:
+
+Administration > Plugins > Configure (in the "OpenProject Help Link Changer" row)
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-help_link", git: "https://github.com/finnlabs/openproject-help_link.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/help-link-changer
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/openproject-help_link`
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+
+Licence
+-------
+
+Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-help_link/app/helpers/openproject_help_link_settings_helper.rb b/vendored-plugins/openproject-help_link/app/helpers/openproject_help_link_settings_helper.rb
new file mode 100644
index 0000000000..ffd733901b
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/app/helpers/openproject_help_link_settings_helper.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenprojectHelpLinkSettingsHelper
+ def help_link_setting
+ Setting.plugin_openproject_help_link['help_link_target']
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/app/views/settings/_openproject_help_link_settings.html.erb b/vendored-plugins/openproject-help_link/app/views/settings/_openproject_help_link_settings.html.erb
new file mode 100644
index 0000000000..2d732fa3d2
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/app/views/settings/_openproject_help_link_settings.html.erb
@@ -0,0 +1,23 @@
+<%#-- copyright
+OpenProject Help Link Plugin
+
+Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%=l('openproject_help_link.setting_help_link_target')%>
+<%= text_field_tag "settings[help_link_target]", help_link_setting, :size => 50 %>
+
diff --git a/vendored-plugins/openproject-help_link/config/locales/da.yml b/vendored-plugins/openproject-help_link/config/locales/da.yml
new file mode 100644
index 0000000000..b918741d29
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/da.yml
@@ -0,0 +1,3 @@
+da:
+ openproject_help_link:
+ setting_help_link_target: URL til hjælpeside
diff --git a/vendored-plugins/openproject-help_link/config/locales/de.yml b/vendored-plugins/openproject-help_link/config/locales/de.yml
new file mode 100644
index 0000000000..9c5fb1df14
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/de.yml
@@ -0,0 +1,3 @@
+de:
+ openproject_help_link:
+ setting_help_link_target: URL für Hilfe-Seite
diff --git a/vendored-plugins/openproject-help_link/config/locales/en.yml b/vendored-plugins/openproject-help_link/config/locales/en.yml
new file mode 100644
index 0000000000..6c7c17fab3
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/en.yml
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# English strings go here for Rails i18n
+en:
+ openproject_help_link:
+ setting_help_link_target: "URL for help page"
diff --git a/vendored-plugins/openproject-help_link/config/locales/es-ES.yml b/vendored-plugins/openproject-help_link/config/locales/es-ES.yml
new file mode 100644
index 0000000000..24493d58fb
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/es-ES.yml
@@ -0,0 +1,3 @@
+es:
+ openproject_help_link:
+ setting_help_link_target: URL para la página de ayuda
diff --git a/vendored-plugins/openproject-help_link/config/locales/fr.yml b/vendored-plugins/openproject-help_link/config/locales/fr.yml
new file mode 100644
index 0000000000..7bc3dfd27f
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/fr.yml
@@ -0,0 +1,3 @@
+fr:
+ openproject_help_link:
+ setting_help_link_target: "URL de la page d'aide"
diff --git a/vendored-plugins/openproject-help_link/config/locales/hu.yml b/vendored-plugins/openproject-help_link/config/locales/hu.yml
new file mode 100755
index 0000000000..e079c21b78
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/hu.yml
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# Hungarian strings go here for Rails i18n
+hu:
+ openproject_help_link:
+ setting_help_link_target: "A Súgó oldal URL-je"
diff --git a/vendored-plugins/openproject-help_link/config/locales/it.yml b/vendored-plugins/openproject-help_link/config/locales/it.yml
new file mode 100644
index 0000000000..ade3ee526d
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/it.yml
@@ -0,0 +1,3 @@
+it:
+ openproject_help_link:
+ setting_help_link_target: URL per la pagina di aiuto
diff --git a/vendored-plugins/openproject-help_link/config/locales/pt-BR.yml b/vendored-plugins/openproject-help_link/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..70d3a876aa
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/pt-BR.yml
@@ -0,0 +1,3 @@
+pt-BR:
+ openproject_help_link:
+ setting_help_link_target: URL para a página de ajuda
diff --git a/vendored-plugins/openproject-help_link/config/locales/ru.yml b/vendored-plugins/openproject-help_link/config/locales/ru.yml
new file mode 100644
index 0000000000..0ecc9655de
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/ru.yml
@@ -0,0 +1,3 @@
+ru:
+ openproject_help_link:
+ setting_help_link_target: URL-адрес страницы справки
diff --git a/vendored-plugins/openproject-help_link/config/locales/sk.yml b/vendored-plugins/openproject-help_link/config/locales/sk.yml
new file mode 100644
index 0000000000..d95540542d
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/sk.yml
@@ -0,0 +1,3 @@
+sk:
+ openproject_help_link:
+ setting_help_link_target: Adresa URL pre stránku nápovedy
diff --git a/vendored-plugins/openproject-help_link/config/locales/sv-SE.yml b/vendored-plugins/openproject-help_link/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..eb3967e359
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/sv-SE.yml
@@ -0,0 +1,3 @@
+sv:
+ openproject_help_link:
+ setting_help_link_target: URL för hjälpsida
diff --git a/vendored-plugins/openproject-help_link/config/locales/tr.yml b/vendored-plugins/openproject-help_link/config/locales/tr.yml
new file mode 100644
index 0000000000..54cbc8d347
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/config/locales/tr.yml
@@ -0,0 +1,3 @@
+tr:
+ openproject_help_link:
+ setting_help_link_target: "Yardım sayfasının URL'si"
diff --git a/vendored-plugins/openproject-help_link/db/migrate/20131023153642_rename_help_link_plugin_settings.rb b/vendored-plugins/openproject-help_link/db/migrate/20131023153642_rename_help_link_plugin_settings.rb
new file mode 100644
index 0000000000..4a41d47e71
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/db/migrate/20131023153642_rename_help_link_plugin_settings.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'setting_renamer').to_s
+class RenameHelpLinkPluginSettings < ActiveRecord::Migration
+ def up
+ Migration::SettingRenamer.rename('plugin_help_link', 'plugin_openproject_help_link')
+ end
+
+ def down
+ Migration::SettingRenamer.rename('plugin_openproject_help_link', 'plugin_help_link')
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/doc/CHANGELOG.md b/vendored-plugins/openproject-help_link/doc/CHANGELOG.md
new file mode 100644
index 0000000000..b685cd8e47
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/doc/CHANGELOG.md
@@ -0,0 +1,93 @@
+
+
+# Changelog
+
+## 3.0.7
+
+* `#5357` Adapt released plugins to base on plugins functionality
+
+## 3.0.6
+
+* Adaptations for new icon font
+
+## 3.0.5
+* `#2221` Renamed Redmine::Info to OpenProject::Info
+* `#2545` Migrated old plugin settings
+
+2013-06-21 Christian Ratz
+
+ * 3.0.3
+ * Use final plugin structure and name schema
+
+2013-06-14 Christian Ratz
+
+ * 3.0.2
+ * added dependency to OpenProject core >= 3.0.0beta1
+
+2013-05-24 Christian Ratz
+
+ * 3.0.1
+ * Rails 3 version of the plugin as a gem
+
+2012-11-22 Jens Ulferts
+
+ * 2.0.1
+ * Format fix in README
+ * 2.0.0
+ * license changed to GPLv3
+ * removes ChiliProject references
+
+2012-11-16 Romano Licker
+
+ * Set default help link target to openproject.org
+
+2012-07-16 Jens Ulferts
+
+ * 1.0.5
+ * Calls menu manager less often in dev mode
+
+2012-07-09 Jens Ulferts
+
+ * 1.0.4
+ * cuke is no longer using selenium
+
+2012-06-28 Jens Ulferts
+
+ * Hungarian translation
+
+2012-03-05 Gregor Schmidt
+
+ * 1.0.3
+ * Adding Credits in README.rdoc
+ * 1.0.2
+ * Changing the default value to point to chiliproject.org instead of redmine.org
+ * Fixing the URL of chiliproject cucumber in README
+ * Adding compatibility statements for ChiliProject 2.x and 3.x
+ * 1.0.1
+
+2011-08-11 Felix Schäfter
+
+ * Fixing a test
+
+2011-07-21 Gregor Schmidt
+
+ * 1.0.0
+ * This plug-in allows you to change the target of the Help link in your ChiliProject.
diff --git a/vendored-plugins/openproject-help_link/doc/COPYRIGHT.md b/vendored-plugins/openproject-help_link/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..4f2da96de7
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/doc/COPYRIGHT.md
@@ -0,0 +1,19 @@
+OpenProject Help Link Plugin
+
+This plugin allows you to change the target of the help link in your
+OpenProject.
+
+Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-help_link/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-help_link/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..eb2d09a472
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/doc/COPYRIGHT_short.md
@@ -0,0 +1,16 @@
+OpenProject Help Link Plugin
+
+Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-help_link/doc/GPL.txt b/vendored-plugins/openproject-help_link/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-help_link/features/link_help_to_project.feature b/vendored-plugins/openproject-help_link/features/link_help_to_project.feature
new file mode 100644
index 0000000000..dfec8aa8f7
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/features/link_help_to_project.feature
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Link help to feature
+ # As a System Admin
+ # I want to relink the help button to a project inside the system
+ # So that I can provide help that is customized for the system
+
+ Scenario: HELP link points to public project inside the system
+ Given there is 1 user with:
+ | login | bob |
+ And there is 1 Project with the following:
+ | Identifier | tas |
+ | Name | Support |
+ | Is_public | true |
+ And the help link setting points to the project "Support"
+ And there is 1 Project with the following:
+ | Name | loginproject |
+ And there is a role "Loginrole"
+ And the user "bob" is a "Loginrole" in the project "loginproject"
+ And I am logged in as "bob"
+ When I follow "Help" within "#top-menu"
+ Then I should be on the page of the project "Support"
diff --git a/vendored-plugins/openproject-help_link/features/settings.feature b/vendored-plugins/openproject-help_link/features/settings.feature
new file mode 100644
index 0000000000..7f14d3d9a5
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/features/settings.feature
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Settings for Help Link
+
+ Scenario: Help Link Settings fields
+ Given I am admin
+ When I go to the help_link settings page
+ Then there should be a "settings[help_link_target]" field
diff --git a/vendored-plugins/openproject-help_link/features/step_definitions/help_relinking.rb b/vendored-plugins/openproject-help_link/features/step_definitions/help_relinking.rb
new file mode 100644
index 0000000000..4fa2248b5c
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/features/step_definitions/help_relinking.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Given /^the help link setting points to the project (.+)$/ do |project_name|
+ project_name.gsub!("\"", '')
+ p = Project.find_by_name(project_name)
+ new_values = Setting.plugin_openproject_help_link
+ new_values['help_link_target'] = "/projects/#{p.identifier}"
+ Setting.plugin_openproject_help_link = new_values
+
+ Redmine::MenuManager.map :top_menu do |menu|
+ menu.delete :help
+ menu.push :help, OpenProject::Info.help_url, last: true
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/features/support/path.rb b/vendored-plugins/openproject-help_link/features/support/path.rb
new file mode 100644
index 0000000000..dcca08df86
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/features/support/path.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module HelpLinkNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the help_link settings page$/
+ '/settings/plugin/openproject_help_link'
+ else
+ super
+ end
+ end
+end
+
+World(HelpLinkNavigationHelpers)
diff --git a/vendored-plugins/openproject-help_link/lib/open_project/help_link.rb b/vendored-plugins/openproject-help_link/lib/open_project/help_link.rb
new file mode 100644
index 0000000000..4c028953c9
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/lib/open_project/help_link.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module HelpLink
+ require 'open_project/help_link/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/lib/open_project/help_link/engine.rb b/vendored-plugins/openproject-help_link/lib/open_project/help_link/engine.rb
new file mode 100644
index 0000000000..562365e4aa
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/lib/open_project/help_link/engine.rb
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/plugins'
+
+require 'rails/engine'
+
+module OpenProject::HelpLink
+ class Engine < ::Rails::Engine
+ engine_name :openproject_help_link
+
+ def self.settings
+ {
+ default: {
+ 'help_link_target' => 'https://www.openproject.org/help'
+ },
+ partial: 'settings/openproject_help_link_settings.html.erb'
+ }
+ end
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-help_link',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0',
+ settings: settings do
+ end
+
+ config.to_prepare do
+ require_dependency 'open_project/help_link/patches/info_patch'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/lib/open_project/help_link/patches/info_patch.rb b/vendored-plugins/openproject-help_link/lib/open_project/help_link/patches/info_patch.rb
new file mode 100644
index 0000000000..73a8611f67
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/lib/open_project/help_link/patches/info_patch.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::HelpLink
+ module Patches
+ module InfoPatch
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ base.class_eval do
+
+ class << self
+ alias_method_chain :help_url, :settings
+ end
+ end
+ end
+
+ module ClassMethods
+ def help_url_with_settings
+ Setting.plugin_openproject_help_link['help_link_target']
+ end
+ end
+ end
+ end
+end
+
+OpenProject::Info.send(:include, OpenProject::HelpLink::Patches::InfoPatch)
diff --git a/vendored-plugins/openproject-help_link/lib/open_project/help_link/version.rb b/vendored-plugins/openproject-help_link/lib/open_project/help_link/version.rb
new file mode 100644
index 0000000000..82739873f6
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/lib/open_project/help_link/version.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module HelpLink
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-help_link/lib/openproject-help_link.rb b/vendored-plugins/openproject-help_link/lib/openproject-help_link.rb
new file mode 100644
index 0000000000..8f5ff77652
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/lib/openproject-help_link.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject Help Link Plugin
+#
+# Copyright (C) 2011 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/help_link'
diff --git a/vendored-plugins/openproject-help_link/openproject-help_link.gemspec b/vendored-plugins/openproject-help_link/openproject-help_link.gemspec
new file mode 100644
index 0000000000..aead796bfb
--- /dev/null
+++ b/vendored-plugins/openproject-help_link/openproject-help_link.gemspec
@@ -0,0 +1,22 @@
+$:.push File.expand_path('../lib', __FILE__)
+
+# Maintain your gem's version:
+require 'open_project/help_link/version'
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = 'openproject-help_link'
+ s.version = OpenProject::HelpLink::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/help-link-changer'
+ s.summary = 'OpenProject Help Link'
+ s.description = "This plugin allows you to change the target of the help link
+ in your OpenProject. Doing so allows you to e.g. have a
+ support project within your OpenProject."
+
+ s.files = Dir['{app,config,db,lib, doc}/**/*'] + ['README.md']
+ s.test_files = Dir['test/**/*']
+
+ s.add_dependency 'rails', '~> 4.2.4'
+end
diff --git a/vendored-plugins/openproject-local_avatars/.gitignore b/vendored-plugins/openproject-local_avatars/.gitignore
new file mode 100644
index 0000000000..757fee31c9
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/.gitignore
@@ -0,0 +1 @@
+/.idea
\ No newline at end of file
diff --git a/vendored-plugins/openproject-local_avatars/README.md b/vendored-plugins/openproject-local_avatars/README.md
new file mode 100644
index 0000000000..18c5e19483
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/README.md
@@ -0,0 +1,42 @@
+# OpenProject Local Avatars Plugin
+
+This plugin allows users to upload an avatar picture to their user profile. So users do not need to use external avatar providers.
+
+## Issue Tracker
+
+https://www.openproject.org/projects/local-avatars/issues
+
+
+## Authors
+
+A. Chaika wrote the original version:
+* http://www.redmine.org/boards/3/topics/5365
+* https://github.com/Ubik/redmine_local_avatars
+
+Luca Pireddu at CRS4 (http://www.crs4.it), contributed updates and improvements.
+
+## Installation
+
+TODO
+
+# Copyright
+
+ * Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+ * OpenProject GmbH
+
+# License
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
diff --git a/vendored-plugins/openproject-local_avatars/app/views/hooks/local_avatars/_view_my_account_contextual.html.erb b/vendored-plugins/openproject-local_avatars/app/views/hooks/local_avatars/_view_my_account_contextual.html.erb
new file mode 100644
index 0000000000..5d891b0cae
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/app/views/hooks/local_avatars/_view_my_account_contextual.html.erb
@@ -0,0 +1 @@
+| <%= link_to(l(:button_change_avatar), :action => 'avatar') %>
diff --git a/vendored-plugins/openproject-local_avatars/app/views/my/avatar.html.erb b/vendored-plugins/openproject-local_avatars/app/views/my/avatar.html.erb
new file mode 100644
index 0000000000..3d009fe6f0
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/app/views/my/avatar.html.erb
@@ -0,0 +1,3 @@
+<% breadcrumb_paths(l(:label_my_account), l(:button_change_avatar)) %>
+<%= toolbar title: l(:button_change_avatar) %>
+<%= render :partial => 'users/avatar' %>
diff --git a/vendored-plugins/openproject-local_avatars/app/views/users/_avatar.html.erb b/vendored-plugins/openproject-local_avatars/app/views/users/_avatar.html.erb
new file mode 100644
index 0000000000..655e305c96
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/app/views/users/_avatar.html.erb
@@ -0,0 +1,30 @@
+ <%= form_tag( { action: 'update_avatar', id: @user }, multipart: true) do %>
+
+ <% end %>
diff --git a/vendored-plugins/openproject-local_avatars/config/locales/de.yml b/vendored-plugins/openproject-local_avatars/config/locales/de.yml
new file mode 100644
index 0000000000..8f7e7e727a
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/config/locales/de.yml
@@ -0,0 +1,13 @@
+# German translation by cforce
+# https://bugs.launchpad.net/redminelocalavatars/+bug/680548
+
+de:
+ label_avatar: "Avatar"
+ message_avatar_uploaded: "Avatar erfolgreich geändert."
+ error_image_upload: "Fehler beim Abspeichern des Bildes."
+ button_change_avatar: "Avatar ändern"
+ are_you_sure_delete_avatar: "Wollen Sie den Avatar wirklich löschen?"
+ avatar_deleted: "Avatar erfolgreich gelöscht."
+ unable_to_delete_avatar: "Avatar konnte nicht gelöscht werden."
+ wrong_file_format: "Zulässige Dateiformate sind jpg, png, gif"
+ empty_file_error: "Bitte wählen Sie ein Bild (jpg, png, gif) aus."
diff --git a/vendored-plugins/openproject-local_avatars/config/locales/en.yml b/vendored-plugins/openproject-local_avatars/config/locales/en.yml
new file mode 100644
index 0000000000..ec2a1151f0
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/config/locales/en.yml
@@ -0,0 +1,14 @@
+# English strings go here
+en:
+ label_avatar: "Avatar"
+ label_current_avatar: "Current Avatar"
+ label_choose_avatar: "Choose Avatar from file"
+ message_avatar_uploaded: "Avatar changed successfully."
+ error_image_upload: "Error saving the image."
+ button_change_avatar: "Change avatar"
+ are_you_sure_delete_avatar: "Are you sure you want to delete your avatar?"
+ avatar_deleted: "Avatar deleted successfully."
+ unable_to_delete_avatar: "Avatar could not be deleted."
+ wrong_file_format: "Allowed formats are jpg, png, gif"
+ empty_file_error: "Please upload a valid image (jpg, png, gif)"
+
diff --git a/vendored-plugins/openproject-local_avatars/config/routes.rb b/vendored-plugins/openproject-local_avatars/config/routes.rb
new file mode 100644
index 0000000000..26d4172ee2
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/config/routes.rb
@@ -0,0 +1,11 @@
+OpenProject::Application.routes.draw do
+
+ post 'my/avatar', :controller => 'my', :action => 'update_avatar', :as => 'my_update_avatar'
+
+ get 'my/avatar', :controller => 'my', :action => 'avatar', :as => 'my_avatar'
+
+ get 'users/:id/avatar', :controller => 'users', :action => 'dump_avatar', :as => 'users_dump_avatar'
+
+ post 'users/:id/avatar', :controller => 'users', :action => 'update_avatar', :as => 'users_update_avatar'
+
+end
diff --git a/vendored-plugins/openproject-local_avatars/features/my_account_local_avatar.feature b/vendored-plugins/openproject-local_avatars/features/my_account_local_avatar.feature
new file mode 100644
index 0000000000..1b123fd6d5
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/features/my_account_local_avatar.feature
@@ -0,0 +1,27 @@
+Feature: Set an new local avatar
+ Background:
+ Given there is 1 user with:
+ | Login | bob |
+ And there is 1 user with the following:
+ | Login | john |
+
+ Scenario: Set an invalid local avatar
+ Given I am already logged in as "john"
+ When I am on the my account page
+ Then I should see "Change avatar" within "#menu-sidebar"
+ When I click on "Change avatar"
+ Then I should be on the my avatar page
+ When I upload a "invalid_avatar.txt" image
+ And I press "Save"
+ Then I should be on the my avatar page
+ And I should see "Allowed formats are jpg, png, gif"
+
+ Scenario: Set a valid local avatar
+ Given I am already logged in as "bob"
+ When I am on the my account page
+ Then I should see "Change avatar" within "#menu-sidebar"
+ When I click on "Change avatar"
+ Then I should be on the my avatar page
+ When I upload a "valid_avatar.png" image
+ And I press "Save"
+ Then I should see "Avatar changed successfully."
diff --git a/vendored-plugins/openproject-local_avatars/features/step_definitions/local_avatar_steps.rb b/vendored-plugins/openproject-local_avatars/features/step_definitions/local_avatar_steps.rb
new file mode 100644
index 0000000000..7bc9dce89f
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/features/step_definitions/local_avatar_steps.rb
@@ -0,0 +1,12 @@
+When /^I upload a "(.*?)" image/ do |image|
+ attach_file(:avatar, File.join(File.dirname(__FILE__), '../upload-files', image.to_s))
+end
+
+Given /^the user "(.*?)" has a local avatar/ do | login |
+ user = User.find_by_login(login)
+ user.attachments = [FactoryGirl.build(:avatar_attachment, :author => user)]
+end
+
+Then /^I should see a local avatar inside "(.*?)"/ do |container|
+ page.should have_selector(".#{container} img.avatar")
+end
diff --git a/vendored-plugins/openproject-local_avatars/features/upload-files/invalid_avatar.txt b/vendored-plugins/openproject-local_avatars/features/upload-files/invalid_avatar.txt
new file mode 100644
index 0000000000..63ebba14d7
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/features/upload-files/invalid_avatar.txt
@@ -0,0 +1 @@
+Just some text in here
diff --git a/vendored-plugins/openproject-local_avatars/features/upload-files/valid_avatar.png b/vendored-plugins/openproject-local_avatars/features/upload-files/valid_avatar.png
new file mode 100644
index 0000000000..84e7b65405
Binary files /dev/null and b/vendored-plugins/openproject-local_avatars/features/upload-files/valid_avatar.png differ
diff --git a/vendored-plugins/openproject-local_avatars/features/user_local_avatar.feature b/vendored-plugins/openproject-local_avatars/features/user_local_avatar.feature
new file mode 100644
index 0000000000..64689f2178
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/features/user_local_avatar.feature
@@ -0,0 +1,15 @@
+Feature: Check adding avatar by admin in the user show page
+ Background:
+ Given there is 1 user with:
+ | Login | bob |
+
+ # FIXME This should be working without javascript, this might be related to imagemagick 2.15.4
+ @javascript
+ Scenario: Set an avatar for custom user
+ Given I am already admin
+ When I edit the user "bob"
+ Then I should see "Avatar" within ".tabs"
+ When I click on "Avatar"
+ And I upload a "valid_avatar.png" image
+ And I press "Save"
+ Then I should see "Avatar changed successfully."
diff --git a/vendored-plugins/openproject-local_avatars/features/work_package_show_local_avatar.feature b/vendored-plugins/openproject-local_avatars/features/work_package_show_local_avatar.feature
new file mode 100644
index 0000000000..b4c3fd35cf
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/features/work_package_show_local_avatar.feature
@@ -0,0 +1,17 @@
+Feature: Check if local avatar was set
+ Background:
+ Given there is 1 user with:
+ | Login | bob |
+ And the user "bob" has a local avatar
+ And the "gravatar_enabled" setting is set to true
+
+ @javascript
+ Scenario: Create a ticket with user that had set an avatar
+ Given there is a project named "iMate"
+ And there are the following work packages in project "iMate":
+ | subject | start_date | due_date | author |
+ | pe2 | 2013-01-01 | 2013-12-31 | bob |
+
+ When I am already admin
+ And I go to the page of the planning element "pe2" of the project called "iMate"
+ Then I should see a local avatar inside "detail-activity"
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars.rb
new file mode 100644
index 0000000000..4e084a428b
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module LocalAvatars
+ require "open_project/local_avatars/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/engine.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/engine.rb
new file mode 100644
index 0000000000..c728a7bf1b
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/engine.rb
@@ -0,0 +1,45 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ class Engine < ::Rails::Engine
+ engine_name :openproject_local_avatars
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-local_avatars',
+ :author_url => 'http://finn.de',
+ :requires_openproject => '>= 4.0.0' do
+
+ add_menu_item :my_menu, :change_avatar,
+ {:controller => 'my', :action => 'avatar'},
+ :caption => :button_change_avatar,
+ :html => { :class => 'icon2 icon-image1' }
+ end
+
+ config.to_prepare do
+ require_dependency 'project'
+ end
+
+ patches [:User,
+ :AvatarHelper,
+ :MyController,
+ :UsersController,
+ :UsersHelper]
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/local_avatars.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/local_avatars.rb
new file mode 100644
index 0000000000..51d26adb5f
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/local_avatars.rb
@@ -0,0 +1,55 @@
+# Redmine Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ module LocalAvatars
+ private
+
+ def save_or_delete_avatar
+ current_attachment = @user.local_avatar_attachment
+ if params[:delete]
+ if current_attachment and current_attachment.destroy
+ flash[:notice] = l(:avatar_deleted)
+ else
+ flash[:error] = l(:unable_to_delete_avatar)
+ false
+ end
+ else
+
+ avatar = params[:avatar];
+ if avatar.nil?
+ flash[:error] = l(:empty_file_error)
+ return
+ end
+
+ unless avatar.original_filename =~ /\.(jpe?g|gif|png)\z/i
+ flash[:error] = l(:wrong_file_format)
+ return
+ end
+
+ begin
+ @user.local_avatar_attachment = avatar
+ flash[:notice] = l(:message_avatar_uploaded)
+ rescue
+ flash[:error] = l(:error_image_upload)
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/avatar_helper_patch.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/avatar_helper_patch.rb
new file mode 100644
index 0000000000..43e223c352
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/avatar_helper_patch.rb
@@ -0,0 +1,69 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ module Patches
+ module AvatarHelperPatch
+ def self.included(base) # :nodoc:
+ base.class_eval do
+
+ def avatar_with_local(user, options = {})
+ local_avatar(user, options) || avatar_without_local(user, options)
+ end
+
+ def avatar_url_with_local(user, options = {})
+ local_avatar_url(user) || avatar_url_without_local(user, options)
+ end
+
+ alias_method_chain :avatar, :local
+ alias_method_chain :avatar_url, :local
+
+ private
+
+ def local_avatar_url(user)
+ with_default_local_avatar_options(user, {}) do |_, _|
+ users_dump_avatar_url(user)
+ end
+ end
+
+ def local_avatar(user, options = {})
+ with_default_local_avatar_options(user, options) do |_, opts|
+ tag_options = merge_image_options(user, opts)
+
+ image_url = users_dump_avatar_url(user)
+
+ tag_options[:src] = image_url
+ tag_options[:alt] = 'Avatar'
+
+ tag 'img', tag_options, false, false
+ end
+ end
+
+ def with_default_local_avatar_options(user, options, &block)
+ return unless user.respond_to?(:local_avatar_attachment) &&
+ user.local_avatar_attachment
+
+ with_default_avatar_options(user, options) do |email, opts|
+ block.call(email, opts)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/my_controller_patch.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/my_controller_patch.rb
new file mode 100644
index 0000000000..5957ba66a2
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/my_controller_patch.rb
@@ -0,0 +1,50 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ module Patches
+ module MyControllerPatch
+ def self.included(base) # :nodoc:
+ base.class_eval do
+ helper :attachments
+ verify :method => :get, :only => :avatar, :render => {:nothing => true, :status => :method_not_allowed}
+ verify :method => :post, :only => :my_update_avatar, :render => {:nothing => true, :status => :method_not_allowed}
+ menu_item :change_avatar, :only => [:avatar]
+
+ include AttachmentsHelper
+ include ::OpenProject::LocalAvatars::LocalAvatars
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def avatar
+ @user = User.current
+ write_settings(redirect_to: :avatar)
+ end
+
+ def update_avatar
+ @user = User.current
+
+ save_or_delete_avatar
+ redirect_to action: 'avatar'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/user_patch.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/user_patch.rb
new file mode 100644
index 0000000000..96e97a8e9a
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/user_patch.rb
@@ -0,0 +1,48 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'rmagick'
+
+module OpenProject::LocalAvatars
+ module Patches
+ module UserPatch
+ def self.included(base) # :nodoc:
+ base.class_eval do
+ acts_as_attachable
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def local_avatar_attachment
+ self.attachments.find_by_description('avatar')
+ end
+
+ def local_avatar_attachment=(file)
+ image = Magick::Image.from_blob(file.read).first
+ image.crop_resized!(128, 128) if image.columns > 128 || image.rows > 128
+ image.write(file.tempfile.path)
+ file.tempfile.rewind
+
+ local_avatar_attachment.destroy if local_avatar_attachment
+ Attachment.attach_files(self, {'first' => {'file' => file, 'description' => 'avatar'}})
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_controller_patch.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_controller_patch.rb
new file mode 100644
index 0000000000..d79839e140
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_controller_patch.rb
@@ -0,0 +1,59 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ module Patches
+ module UsersControllerPatch
+
+ def self.included(base) # :nodoc:
+ base.class_eval do
+ helper :attachments
+ verify :method => :get, :only => :dump_avatar, :render => {:nothing => true, :status => :method_not_allowed}
+ verify :method => :post, :only => :update_avatar, :render => {:nothing => true, :status => :method_not_allowed}
+ skip_before_filter :require_admin, :only => :dump_avatar
+
+ include AttachmentsHelper
+ include ::OpenProject::LocalAvatars::LocalAvatars
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def dump_avatar
+ return unless find_user
+ av = @user.local_avatar_attachment
+ unless av
+ render_404
+ else
+
+ send_file(av.diskfile, :filename => filename_for_content_disposition(av.filename),
+ :type => av.content_type,
+ :disposition => (av.image? ? 'inline' : 'attachment'))
+ end
+ end
+
+ def update_avatar
+ return unless find_user
+
+ save_or_delete_avatar
+ redirect_to action: 'edit', id: @user, tab: 'avatar'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_helper_patch.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_helper_patch.rb
new file mode 100644
index 0000000000..2f1912a392
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/patches/users_helper_patch.rb
@@ -0,0 +1,37 @@
+# OpenProject Local Avatars plugin
+#
+# Copyright (C) 2010 Andrew Chaika, Luca Pireddu
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module OpenProject::LocalAvatars
+ module Patches
+ module UsersHelperPatch
+ def self.included(base) # :nodoc:
+ base.send :include, InstanceMethods
+ base.class_eval do
+ alias_method_chain :user_settings_tabs, :avatar unless method_defined?(:user_settings_tabs_without_avatar)
+ end
+ end
+
+ module InstanceMethods
+ def user_settings_tabs_with_avatar
+ tabs = user_settings_tabs_without_avatar
+ tabs << {:name => 'avatar', :partial => 'users/avatar', :label => :label_avatar}
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/version.rb b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/version.rb
new file mode 100644
index 0000000000..9bbd74c71b
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/open_project/local_avatars/version.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module LocalAvatars
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/lib/openproject-local_avatars.rb b/vendored-plugins/openproject-local_avatars/lib/openproject-local_avatars.rb
new file mode 100644
index 0000000000..bbea30bf58
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/lib/openproject-local_avatars.rb
@@ -0,0 +1 @@
+require 'open_project/local_avatars'
diff --git a/vendored-plugins/openproject-local_avatars/openproject-local_avatars.gemspec b/vendored-plugins/openproject-local_avatars/openproject-local_avatars.gemspec
new file mode 100644
index 0000000000..eae28b4379
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/openproject-local_avatars.gemspec
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/local_avatars/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-local_avatars"
+ s.version = OpenProject::LocalAvatars::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/local-avatars"
+ s.summary = 'OpenProject Local Avatars'
+ s.description = 'This plugin allows OpenProject users to upload a picture to be used as
+ an avatar (instead of depending on images from Gravatar).'
+ s.license = 'GPLv3'
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + %w(README.md)
+ s.test_files = Dir["spec/**/*"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency 'rmagick', '~> 2.15.4'
+
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/controllers/my_controller_spec.rb b/vendored-plugins/openproject-local_avatars/spec/controllers/my_controller_spec.rb
new file mode 100644
index 0000000000..c4f4d337ae
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/controllers/my_controller_spec.rb
@@ -0,0 +1,75 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/../shared_examples')
+
+describe MyController, :type => :controller do
+ include_examples "a controller with avatar features"
+
+ describe "GET /my/avatar" do
+ let(:user) { user_without_avatar }
+ before{ allow(User).to receive(:current).and_return user }
+ let(:do_action) { get 'avatar' }
+ it { do_action; expect(assigns(:user)).to eq(user) }
+ it { do_action; is_expected.to render_template 'my/avatar' }
+ end
+
+ describe "GET /my/avatar/update" do
+ before{ user.save; allow(User).to receive(:current).and_return user }
+ context "WHEN save submit" do
+ let(:submit_param) { {:commit => :button_save, :avatar => avatar_file} }
+ context "for a user without an avatar" do
+ let(:user) { user_without_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param }
+ context "WHEN save is successful" do
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to({ controller: 'my', action: 'avatar' }) }
+ specify { expect(user).to receive(:local_avatar_attachment=); do_action }
+ it { do_action; expect(flash[:notice]).to include "changed" }
+ end
+ context "WHEN save is not successful" do
+ before { allow(user).to receive(:local_avatar_attachment=).and_raise(RuntimeError) }
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to my_avatar_path }
+ it { do_action; expect(flash[:error]).to include "Error"}
+ end
+ end
+
+ context "for a user with an avatar" do
+ let(:user) { user_with_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param }
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to({ controller: 'my', action: 'avatar' }) }
+ it_should_behave_like "an action that deletes the user's avatar"
+ specify { expect(Attachment).to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to include "changed" }
+ end
+ end
+
+ describe "WHEN delete submit" do
+ let(:submit_param) { {:delete => :true} }
+ context "for a user without an avatar" do
+ let(:user) { user_without_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param }
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to my_avatar_path }
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:error]).to include "could not be deleted" }
+ it { do_action; expect(user.local_avatar_attachment).to be_blank}
+ end
+
+ context "for a user with an avatar" do
+ let(:user) { user_with_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param }
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to({ controller: 'my', action: 'avatar' }) }
+ it_should_behave_like "an action that deletes the user's avatar"
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to include "deleted successfully" }
+ it { do_action; expect(user.local_avatar_attachment).to be_blank}
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/controllers/users_controller_spec.rb b/vendored-plugins/openproject-local_avatars/spec/controllers/users_controller_spec.rb
new file mode 100644
index 0000000000..8093461045
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/controllers/users_controller_spec.rb
@@ -0,0 +1,117 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/../shared_examples')
+
+describe UsersController, :type => :controller do
+ include_examples "a controller with avatar features"
+
+ describe "GET /users/:id/dump_avatar" do
+ let(:user) { user_with_avatar }
+ let(:action) { get :dump_avatar, :id => user.id }
+ let(:redirect_path) { users_dump_avatar_url(:id => user.id) }
+ it_should_behave_like "an action checked for required login"
+
+ context "for an invalid user" do
+ let(:do_action) { get :dump_avatar, :id => 0}
+ it_should_behave_like "an action with an invalid user"
+ end
+ #
+ context "for a user without an avatar" do
+ let(:user) { user_without_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { get :dump_avatar, :id => user.id}
+ it_should_behave_like "an action with an invalid user"
+ end
+ #
+ context "for a user with an avatar" do
+ let(:user) { user_with_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { get :dump_avatar, :id => user.id }
+ it { do_action; expect(response).to be_success }
+ #again, we have to use the nasty trick to catch the default rails render
+ it { expect(@controller).to receive(:send_file).and_return true; expect(@controller).to receive(:render); do_action }
+ end
+ end
+
+ describe "POST /users/:id/update_avatar" do
+ let(:user) { user_with_avatar }
+ let(:action) { post :update_avatar, :avatar => avatar_file, :id => user.id }
+ let(:redirect_path) { users_update_avatar_url(:id => user.id) }
+ let(:successful_response) do
+ expect(response).to redirect_to({ controller: 'users',
+ action: 'edit',
+ id: user.id,
+ tab: 'avatar'} )
+ end
+ it_should_behave_like "an action requiring admin"
+
+ let(:current) { FactoryGirl.create(:admin)}
+
+ before do
+ allow(User).to receive(:current).and_return(current)
+ end
+
+ context "WHEN save submit" do
+ let(:submit_param) { {:commit => :button_save, :avatar => avatar_file} }
+ context "for an invalid user" do
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => 0)}
+ it_should_behave_like "an action with an invalid user"
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to be_blank }
+ end
+
+ context "for a user without an avatar" do
+ let(:user) { user_without_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => user.id)}
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to tab_edit_user_path(user.id, tab: 'avatar') }
+ specify { expect(Attachment).to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to include "changed" }
+ end
+
+ context "for a user with an avatar" do
+ let(:user) { user_with_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => user.id)}
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to tab_edit_user_path(user.id, tab: 'avatar') }
+ it_should_behave_like "an action that deletes the user's avatar"
+ specify { expect(Attachment).to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to include "changed" }
+ end
+ end
+
+ describe "WHEN delete submit" do
+ let(:submit_param) { {:delete => :true} }
+ context "for an invalid user" do
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => 0)}
+ it_should_behave_like "an action with an invalid user"
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to be_blank }
+ end
+
+ context "for a user without an avatar" do
+ let(:user) { user_without_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => user.id)}
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to tab_edit_user_path(user.id, tab: 'avatar') }
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:error]).to include "could not be deleted" }
+ it { do_action; expect(user.local_avatar_attachment).to be_blank}
+ end
+
+ context "for a user with an avatar" do
+ let(:user) { user_with_avatar }
+ it_should_behave_like "an action with stubbed User.find"
+ let(:do_action) { post :update_avatar, submit_param.merge(:id => user.id)}
+ it { do_action; expect(response).to be_redirect }
+ it { do_action; is_expected.to redirect_to tab_edit_user_path(user.id, tab: 'avatar') }
+ it_should_behave_like "an action that deletes the user's avatar"
+ specify { expect(Attachment).not_to receive(:attach_files); do_action }
+ it { do_action; expect(flash[:notice]).to include "deleted successfully" }
+ it { do_action; expect(user.local_avatar_attachment).to be_blank}
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/factories/avatar_attachment_factory.rb b/vendored-plugins/openproject-local_avatars/spec/factories/avatar_attachment_factory.rb
new file mode 100644
index 0000000000..dfe8dfe8fd
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/factories/avatar_attachment_factory.rb
@@ -0,0 +1,14 @@
+FactoryGirl.define do
+ factory :avatar_attachment, class: Attachment do
+ author factory: :user
+ container factory: :work_package
+ description "avatar"
+ filename "avatar.jpg"
+ content_type "image/jpeg"
+ file do
+ OpenProject::Files.create_uploaded_file name: filename,
+ content_type: content_type,
+ binary: true
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/factories/user_factory.rb b/vendored-plugins/openproject-local_avatars/spec/factories/user_factory.rb
new file mode 100644
index 0000000000..b22e622916
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/factories/user_factory.rb
@@ -0,0 +1,7 @@
+FactoryGirl.modify do
+ factory :user do
+ after(:create) do |user|
+ user.password = nil
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/helpers/avatar_helper_spec.rb b/vendored-plugins/openproject-local_avatars/spec/helpers/avatar_helper_spec.rb
new file mode 100644
index 0000000000..545f00b0ca
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/helpers/avatar_helper_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe AvatarHelper, :type => :helper do
+ let(:user) { FactoryGirl.build_stubbed(:user) }
+ let(:avatar_stub) { FactoryGirl.build_stubbed(:avatar_attachment) }
+
+ before do
+ allow(GravatarImageTag.configuration).to receive(:secure).and_return(false)
+ allow(user).to receive(:local_avatar_attachment).and_return avatar_stub
+ end
+
+ def expected_image_tag(user)
+ tag_options = { title: user.name,
+ alt: 'Avatar',
+ class: 'avatar' }
+
+ image_tag expected_url(user), tag_options
+ end
+
+ def expected_url(user)
+ users_dump_avatar_url(user)
+ end
+
+ def expected_gravatar_url(user)
+ digest = Digest::MD5.hexdigest(user.mail)
+ host = "http://gravatar.com"
+
+ "#{host}/avatar/#{digest}?secure=false"
+ end
+
+ def expected_gravatar_image_tag(user)
+ tag_options = { title: user.name,
+ alt: 'Gravatar',
+ class: 'avatar' }
+
+ image_tag expected_gravatar_url(user), tag_options
+ end
+
+ describe '#avatar' do
+ it "should return the image attached to the user" do
+ with_settings gravatar_enabled: '1' do
+ expect(helper.avatar(user)).to be_html_eql(expected_image_tag(user))
+ end
+ end
+
+ it "should return the gravatar image if no image uploaded for the user" do
+ allow(user).to receive(:local_avatar_attachment).and_return nil
+
+ with_settings gravatar_enabled: '1' do
+ expect(helper.avatar(user)).to be_html_eql(expected_gravatar_image_tag(user))
+ end
+ end
+
+ it "should return blank if image attached to the user but gravatars disabled" do
+ with_settings gravatar_enabled: '0' do
+ expect(helper.avatar(user)).to be_blank
+ end
+ end
+ end
+
+ describe '#avatar_url' do
+ it "should return the url to the image attached to the user" do
+ with_settings gravatar_enabled: '1' do
+ expect(helper.avatar_url(user)).to eq(expected_url(user))
+ end
+ end
+
+ it "should return the gravatar url if no image uploaded for the user" do
+ allow(user).to receive(:local_avatar_attachment).and_return nil
+
+ with_settings gravatar_enabled: '1' do
+ expect(helper.avatar_url(user)).to eq(expected_gravatar_url(user))
+ end
+ end
+
+ it "should return blank if image attached to the user but gravatars disabled" do
+ with_settings gravatar_enabled: '0' do
+ expect(helper.avatar_url(user)).to be_blank
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/lib/users_helper_spec.rb b/vendored-plugins/openproject-local_avatars/spec/lib/users_helper_spec.rb
new file mode 100644
index 0000000000..dd6b657cb7
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/lib/users_helper_spec.rb
@@ -0,0 +1,12 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+class UsersHelperTest
+ include UsersHelper
+end
+
+describe UsersHelperTest do
+ describe "#user_settings_tabs" do
+ subject {UsersHelperTest.new.user_settings_tabs}
+ it { is_expected.to include({:name => 'avatar', :partial => 'users/avatar', :label => :label_avatar}) }
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/models/user_spec.rb b/vendored-plugins/openproject-local_avatars/spec/models/user_spec.rb
new file mode 100644
index 0000000000..a66b486c5c
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/models/user_spec.rb
@@ -0,0 +1,38 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/../shared_examples')
+
+describe User, :type => :model do
+ include_examples "there are users with and without avatars"
+ let(:user) { FactoryGirl.build :user }
+
+ specify { expect(user.attachments).to all be_a_kind_of Attachment }
+
+ describe "#local_avatar_attachment" do
+ subject { user.local_avatar_attachment }
+
+ context "WHEN user has an avatar" do
+ let(:user) {user_with_avatar}
+ it { is_expected.to be_a_kind_of Attachment }
+ end
+
+ context "WHEN user has no avatar" do
+ let(:user) {user_without_avatar}
+ it { is_expected.to be_blank }
+ end
+ end
+
+ describe "#local_avatar_attachment=" do
+ context "WHEN the uploaded file is not an image" do
+ subject { lambda{ user.local_avatar_attachment = bogus_avatar_file } }
+ let(:rescue_block) { lambda{ begin; subject; rescue; false end } }
+ it { is_expected.to raise_error }
+ specify { expect(rescue_block).not_to change(user, :local_avatar_attachment) }
+ end
+
+ context "WHEN the uploaded file is a good image" do
+ subject { lambda{ user.local_avatar_attachment = avatar_file } }
+ it { is_expected.not_to raise_error }
+ specify { is_expected.to change(user, :local_avatar_attachment) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/shared_examples.rb b/vendored-plugins/openproject-local_avatars/spec/shared_examples.rb
new file mode 100644
index 0000000000..c85812ab0b
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/shared_examples.rb
@@ -0,0 +1,149 @@
+shared_examples_for "an action checked for required login" do
+ before do
+ allow(Setting).to receive(:login_required?).and_return(false)
+ end
+
+ describe "WITH no login required" do
+ before do
+ action
+ end
+
+ it "should be success" do
+ expect(response).to be_success
+ end
+ end
+
+ describe "WITH login required" do
+ before do
+ allow(Setting).to receive(:login_required?).and_return(true)
+ action
+ end
+
+ it "should redirect to the login page" do
+ expect(response).to redirect_to signin_path(:back_url => redirect_path)
+ end
+ end
+end
+
+shared_examples_for "an action requiring login" do
+ let(:current) { FactoryGirl.create(:user) }
+
+ before do
+ allow(User).to receive(:current).and_return(current)
+ end
+
+ describe "without beeing logged in" do
+ before do
+ allow(User).to receive(:current).and_return AnonymousUser.first
+
+ action
+ end
+
+ it { expect(response).to redirect_to signin_path(:back_url => redirect_path) }
+ end
+
+ describe "with beeing logged in" do
+ before do
+ action
+ end
+
+ it { expect(response).to be_success }
+ end
+end
+
+
+shared_examples_for "an action requiring admin" do
+ let(:current) { FactoryGirl.create(:admin) }
+
+ before do
+ allow(User).to receive(:current).and_return(current)
+ end
+
+ describe "without beeing logged in" do
+ before do
+ allow(User).to receive(:current).and_return AnonymousUser.first
+
+ action
+ end
+
+ it { expect(response).to redirect_to signin_path(:back_url => redirect_path) }
+ end
+
+ describe "with beeing logged in as a normal user" do
+ before do
+ allow(User).to receive(:current).and_return FactoryGirl.create(:user)
+
+ action
+ end
+
+ it { expect(response.response_code).to eq(403) }
+ end
+
+ describe "with beeing logged in as admin" do
+ before do
+ action
+ end
+
+ it do
+ if respond_to? :successful_response
+ successful_response
+ else
+ expect(response).to be_success
+ end
+ end
+ end
+end
+#
+shared_examples_for "there are users with and without avatars" do
+ let(:user_without_avatar) {FactoryGirl.create (:user)}
+ let(:user_with_avatar) do
+ u = FactoryGirl.create :user
+ u.attachments = [FactoryGirl.build(:avatar_attachment, :author => u)]
+ u
+ end
+ let(:avatar_file) do
+ image = Magick::Image.new(200,200)
+ image.format = "PNG"
+ file = Tempfile.new(['avatar','.png'], :encoding => 'ascii-8bit')
+ file.write image.to_blob
+ file.rewind
+
+ testfile = Rack::Test::UploadedFile.new(file.path, 'avatar.png')
+ allow(testfile).to receive(:tempfile).and_return(file)
+ testfile
+ end
+ let(:bogus_avatar_file) do
+ file = Tempfile.new(['bogus'],['.png'])
+ file.write "alert('Bogus')"
+ file.rewind
+ testfile = Rack::Test::UploadedFile.new(file.path, 'bogus.png')
+ allow(testfile).to receive(:tempfile).and_return(file)
+ testfile
+ end
+end
+#
+shared_examples_for "a controller with avatar features" do
+
+ include_examples "there are users with and without avatars"
+ before do
+ allow(User).to receive(:current).and_return FactoryGirl.create(:anonymous)
+ allow(File).to receive(:delete).and_return true
+ end
+
+end
+#
+shared_examples_for "an action with an invalid user" do
+ it { do_action; expect(response).not_to be_success }
+ it { do_action; expect(response.code).to eq("404")}
+end
+
+shared_examples_for "an action with stubbed User.find" do
+ before do
+ allow(user).to receive(:save).and_return true if user
+ allow(User).to receive(:find) { |id, args| (id.to_s == "0") ? nil : user }
+ end
+end
+#
+shared_examples_for "an action that deletes the user's avatar" do
+ it { expect_any_instance_of(Attachment).to receive(:destroy).and_call_original; do_action }
+end
diff --git a/vendored-plugins/openproject-local_avatars/spec/spec_helper.rb b/vendored-plugins/openproject-local_avatars/spec/spec_helper.rb
new file mode 100644
index 0000000000..f8ec36959d
--- /dev/null
+++ b/vendored-plugins/openproject-local_avatars/spec/spec_helper.rb
@@ -0,0 +1 @@
+require 'spec_helper'
diff --git a/vendored-plugins/openproject-meeting/.hound.yml b/vendored-plugins/openproject-meeting/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-meeting/.rubocop.yml b/vendored-plugins/openproject-meeting/.rubocop.yml
new file mode 100644
index 0000000000..799fa65a30
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - "*.gemspec"
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
\ No newline at end of file
diff --git a/vendored-plugins/openproject-meeting/.travis.yml b/vendored-plugins/openproject-meeting/.travis.yml
new file mode 100644
index 0000000000..d8a20e3fc2
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/.travis.yml
@@ -0,0 +1,111 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Travis configuration based on the respective OpenProject core configuration.
+# Everything save for the matrix section and additional `before_install`
+# instructions is copied and pasted from the core.
+
+language: ruby
+
+rvm:
+ - 2.2.3
+
+sudo: false
+
+cache:
+ - bundler: true
+ - directories:
+ - frontend/node_modules
+ - frontend/bower_components
+
+bundler_args: --without development production
+
+branches:
+ only:
+ - master
+ - dev
+ - /^(stable|release)\/.*$/
+
+env:
+ global:
+ - CI=true
+ - RAILS_ENV=test
+ - COVERAGE=true
+
+ matrix:
+ - "TEST_SUITE=plugins:spec DB=mysql"
+ - "TEST_SUITE=plugins:cucumber DB=mysql"
+
+before_install:
+ # Custom plugin instructions follow.
+
+ # Move the plugin into a subfolder. The plugin-provided Gemfile.plugins
+ # must refer to this folder.
+ - mkdir -p plugins/this
+ - echo `ls -a | tail -n+3 | grep -v plugins` plugins/this/ | xargs mv
+
+ # Get OpenProject.
+ # Doing the fetch detour as you cannot clone into the current directory.
+ - git init
+ - git remote add openproject https://github.com/opf/openproject.git
+ - git fetch --depth=1 openproject
+ - git checkout openproject/dev
+
+ # End of custom plugin instructions.
+
+ - "echo `firefox -v`"
+ - "export DISPLAY=:99.0"
+ - "/sbin/start-stop-daemon --start -v --pidfile ./tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x16"
+ - "echo `xdpyinfo -display :99 | grep 'dimensions' | awk '{ print $2 }'`"
+ - travis_retry npm install
+
+ # We need phantomjs 2.0 to get tests passing
+ - mkdir travis-phantomjs
+ - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2
+ - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs
+ - export PATH=$PWD/travis-phantomjs:$PATH
+
+before_script:
+ - sh script/ci_setup.sh $DB
+
+script:
+ - sh script/ci_runner.sh $TEST_SUITE $GROUP_SIZE $GROUP
+
+notifications:
+ email: false
+ slack:
+ secure: "a+I0uMgXgrDd3aitr2yhXrh7g/UOUTwoDVElunY7gYdrM+gpZ6RE1AP4/Q++hERBCs7rUBzmb//zxGTcc8Nw4nGqZOmPOMIsAoD49UupGLUzHbxzKlpwdBcwh77fq3rYwkjZjE/H1qiElPT7v6qyWMSdNGlj/bAB74eD7Zl3S5cMRvZ1whbSg2GA2v6ZqtXaKfrSFrPRzsIOBXs99OxWNWAsUGpEwTYac7wb6rdMJkBbzosp4gP99mGvQArEzo0nrIQgRH8W4Q6iLnrpX0g5uKccWl1u/G2bmH8L4F50ce4uuUE+TtHO/nfNFnb2KuDR4QyoccQQbGHXL/jaaAZXG/gzs5Hmru2Thaym43fSwxos80xmZs1vqB/rXE+Rg9qXcCKyyX31zjSI/iW4wS015fz8MKVX6qDg49epaw1ovn0AOYrvTd94GV6RX6eJ3/l+KJJHSKaaLP/713h11LWx/S27tiB40fboXQ68YzIQCuahRUEHUfhU3P10Wf9y2fdDsthtHHSrOJMQ3Ii/Jm3KQm6bE5RWORdHvc/sF2WLfLmJ627j9JhWYYi5mDKJ9AeMWtZNHreU0mM27OUgfhiW11ItKgpwQPEiiicrlYRrMmK+9hc9cym+8tRM+wEth1xhIkfgQFtngONKjv361Wt3JifxM79+bn0IyF72vAVNy8k="
+
+addons:
+ firefox: "38.0esr"
+ postgresql: "9.3"
+
+# Disabling coverage reporting until CodeClimate supports merging results from multiple partial tests
+# code_climate:
+# repo_token:
+# secure: "W/lyd8Ud18GRASuVShsIKa2MRHhxjh8WICMQ4WKr68qt0X0Tlp7Bclv4ReiEgiQeKsIoJJy5FfJfINdAT8A4sy2JbrLeISShcIU7Kqpfh6DSLNoRAuLz5P7EXMNFns1gBKCmrSzcB+9ksuTLyTCKkjUcj1NbJzGqpB4jSTecAdg="
diff --git a/vendored-plugins/openproject-meeting/Gemfile.plugins b/vendored-plugins/openproject-meeting/Gemfile.plugins
new file mode 100644
index 0000000000..b1d715029e
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/Gemfile.plugins
@@ -0,0 +1,7 @@
+# Used by travis to bundle this plugin with the OpenProject core.
+# The tested plugin will be moved to the path `./plugins/this`
+# whereas OpenProject will be checked out to `.`.
+
+gem 'openproject-meeting', path: 'plugins/this'
+
+# If the plugin has any dependencies declare them here:
diff --git a/vendored-plugins/openproject-meeting/README.md b/vendored-plugins/openproject-meeting/README.md
new file mode 100644
index 0000000000..49ef9b007d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/README.md
@@ -0,0 +1,78 @@
+OpenProject Meeting Plugin
+==========================
+
+This plugin adds functions to support project meetings to
+[OpenProject](https://www.openproject.org). Meetings
+can be scheduled selecting invitees from the same project to take
+part in the meeting. An agenda can be created and sent to the invitees.
+After the meeting, attendees can be selected and minutes can be
+created based on the agenda. Finally, the minutes can be sent to
+all attendees and invitees.
+
+A more detailed feature tour can be found [here](https://www.openproject.org/projects/openproject/wiki/Meetings).
+
+Requirements
+------------
+
+The Meeting plugin currently requires the [OpenProject Core](https://github.com/opf/openproject/) in
+version greater or equal to 3.0.0.
+
+
+Installation
+------------
+
+Add the following line to the `Gemfile.plugins` to your OpenProject installation (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-meeting", :git => "https://github.com/finnlabs/openproject-meeting.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-meeting", :git => "https://github.com/finnlabs/openproject-meeting.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this leaves plugin data in the database. Currently, we do not
+support full uninstall of the plugin.
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/plugin-meetings
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+
+`https://github.com/finnlabs/openproject-meeting`
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+* Vincent Le Moign and his fabulous Minicons icons on [webalys.com](http://www.webalys.com/minicons/icons-free-pack.php)
+
+License
+-------
+
+(c) 2011 - 2014 - the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and
+doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-meeting/app/assets/stylesheets/meeting/meeting.css.erb b/vendored-plugins/openproject-meeting/app/assets/stylesheets/meeting/meeting.css.erb
new file mode 100644
index 0000000000..531a32af03
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/assets/stylesheets/meeting/meeting.css.erb
@@ -0,0 +1,45 @@
+/*-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++*/
+
+.meetings div.meeting {padding-bottom: 1em;}
+div.meeting_content {padding-bottom: 2em;}
+div.meetings_by_date {padding: 1em 0 1em 2em;}
+dl.meetings p, div#activity dl.meetings dd, div#activity dl.meetings dt {padding: 0;}
+dl.meetings {margin-bottom: 2em;}
+
+div.tabular > div { margin: 0;
+ padding: 5px 0 8px 0;
+ padding-left: 180px; /*width of left column containing the label elements*/
+ height: 1%;
+ clear:left;
+}
+
+#meeting_agenda_preview fieldset {margin-top: 1em; background: url(<%= asset_path 'draft.png' %>);}
+#meeting_minutes_preview fieldset {margin-top: 1em; background: url(<%= asset_path 'draft.png' %>);}
+
+div.flash.notice a.link_to_profile {
+ cursor:pointer;
+ color: #008BD0;
+}
+
+div.flash.notice a.link_to_profile:hover {
+ text-decoration: underline;
+}
diff --git a/vendored-plugins/openproject-meeting/app/controllers/meeting_agendas_controller.rb b/vendored-plugins/openproject-meeting/app/controllers/meeting_agendas_controller.rb
new file mode 100644
index 0000000000..2c39c297a1
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/controllers/meeting_agendas_controller.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingAgendasController < MeetingContentsController
+ menu_item :meetings
+
+ def close
+ @meeting.close_agenda_and_copy_to_minutes!
+
+ redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting
+ end
+
+ def open
+ @content.unlock!
+ redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting
+ end
+
+ private
+
+ def find_content
+ @content = @meeting.agenda || @meeting.build_agenda
+ @content_type = 'meeting_agenda'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/controllers/meeting_contents_controller.rb b/vendored-plugins/openproject-meeting/app/controllers/meeting_contents_controller.rb
new file mode 100644
index 0000000000..b483360931
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/controllers/meeting_contents_controller.rb
@@ -0,0 +1,134 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingContentsController < ApplicationController
+ include PaginationHelper
+ include OpenProject::Concerns::Preview
+
+ menu_item :meetings
+
+ helper :watchers
+ helper :wiki
+ helper :meetings
+ helper :meeting_contents
+ helper :watchers
+ helper :meetings
+
+ before_filter :find_meeting, :find_content
+ before_filter :authorize
+
+ def show
+ if params[:id].present? && @content.version == params[:id].to_i
+ # Redirect links to the last version
+ redirect_to controller: '/meetings',
+ action: :show,
+ id: @meeting,
+ tab: @content_type.sub(/^meeting_/, '')
+ return
+ end
+ # go to an old version if a version id is given
+ @content = @content.at_version params[:id] unless params[:id].blank?
+ render 'meeting_contents/show'
+ end
+
+ def update
+ (render_403; return) unless @content.editable? # TODO: not tested!
+ @content.attributes = content_params
+ @content.author = User.current
+ if @content.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting
+ else
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ params[:tab] ||= 'minutes' if @meeting.agenda.present? && @meeting.agenda.locked?
+ render 'meetings/show'
+ end
+
+ def history
+ # don't load text
+ @content_versions = @content.journals.select('id, user_id, notes, created_at, version')
+ .order('version DESC')
+ .page(page_param)
+ .per_page(per_page_param)
+
+ render 'meeting_contents/history', layout: !request.xhr?
+ end
+
+ def diff
+ @diff = @content.diff(params[:version_to], params[:version_from])
+ render 'meeting_contents/diff'
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def notify
+ unless @content.new_record?
+ author_mail = @content.meeting.author.mail
+ do_not_notify_author = @content.meeting.author.preference[:no_self_notified]
+
+ recipients_with_errors = []
+ @content.meeting.participants.each do |recipient|
+ begin
+ next if recipient.mail == author_mail && do_not_notify_author
+ MeetingMailer.content_for_review(@content, @content_type, recipient.mail).deliver_now
+ rescue
+ recipients_with_errors << recipient
+ end
+ end
+ if recipients_with_errors == []
+ flash[:notice] = l(:notice_successful_notification)
+ else
+ flash[:error] = l(:error_notification_with_errors,
+ recipients: recipients_with_errors.map(&:name).join('; '))
+ end
+ end
+ redirect_back_or_default controller: '/meetings', action: 'show', id: @meeting
+ end
+
+ def default_breadcrumb
+ MeetingsController.new.send(:default_breadcrumb)
+ end
+
+ private
+
+ def find_meeting
+ @meeting = Meeting.includes(:project, :author, :participants, :agenda, :minutes)
+ .find(params[:meeting_id])
+ @project = @meeting.project
+ @author = User.current
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def parse_preview_data
+ text = {}
+
+ text = { WikiContent.human_attribute_name(:content) => content_params[:text] } if @content.editable?
+
+ [text, [], @content]
+ end
+
+ def content_params
+ params.require(@content_type).permit(:text, :lock_version, :comment)
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/controllers/meeting_minutes_controller.rb b/vendored-plugins/openproject-meeting/app/controllers/meeting_minutes_controller.rb
new file mode 100644
index 0000000000..dc8397449c
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/controllers/meeting_minutes_controller.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingMinutesController < MeetingContentsController
+ menu_item :meetings
+
+ private
+
+ def find_content
+ @content = @meeting.minutes || @meeting.build_minutes
+ @content_type = 'meeting_minutes'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/controllers/meetings_controller.rb b/vendored-plugins/openproject-meeting/app/controllers/meetings_controller.rb
new file mode 100644
index 0000000000..6259ac682b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/controllers/meetings_controller.rb
@@ -0,0 +1,169 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingsController < ApplicationController
+ around_filter :set_time_zone
+ before_filter :find_project, only: [:index, :new, :create]
+ before_filter :find_meeting, except: [:index, :new, :create]
+ before_filter :convert_params, only: [:create, :update]
+ before_filter :authorize
+
+ helper :journals
+ helper :watchers
+ helper :meeting_contents
+ include WatchersHelper
+ include PaginationHelper
+
+ menu_item :new_meeting, only: [:new, :create]
+
+ def index
+ scope = @project.meetings
+
+ # from params => today's page otherwise => first page as fallback
+ tomorrows_meetings_count = scope.from_tomorrow.count
+ @page_of_today = 1 + tomorrows_meetings_count / per_page_param
+
+ page = params['page'] ?
+ page_param :
+ @page_of_today
+
+ @meetings = scope.with_users_by_date
+ .page(page)
+ .per_page(per_page_param)
+
+ @meetings_by_start_year_month_date = Meeting.group_by_time(@meetings)
+ end
+
+ def show
+ params[:tab] ||= 'minutes' if @meeting.agenda.present? && @meeting.agenda.locked?
+ end
+
+ def create
+ @meeting.participants.clear # Start with a clean set of participants
+ @meeting.participants_attributes = @converted_params.delete(:participants_attributes)
+ @meeting.attributes = @converted_params
+ if params[:copied_from_meeting_id].present? && params[:copied_meeting_agenda_text].present?
+ @meeting.agenda = MeetingAgenda.new(
+ text: params[:copied_meeting_agenda_text],
+ comment: "Copied from Meeting ##{params[:copied_from_meeting_id]}")
+ @meeting.agenda.author = User.current
+ end
+ if @meeting.save
+ text = l(:notice_successful_create)
+ if User.current.time_zone.nil?
+ link = l(:notice_timezone_missing, zone: Time.zone)
+ text += " #{view_context.link_to(link, { controller: '/my', action: :account }, class: 'link_to_profile')}"
+ end
+ flash[:notice] = text.html_safe
+
+ redirect_to action: 'show', id: @meeting
+ else
+ render action: 'new', project_id: @project
+ end
+ end
+
+ def new
+ end
+
+ def copy
+ params[:copied_from_meeting_id] = @meeting.id
+ params[:copied_meeting_agenda_text] = @meeting.agenda.text if @meeting.agenda.present?
+ @meeting = @meeting.copy(author: User.current)
+ render action: 'new', project_id: @project
+ end
+
+ def destroy
+ @meeting.destroy
+ flash[:notice] = l(:notice_successful_delete)
+ redirect_to action: 'index', project_id: @project
+ end
+
+ def edit
+ end
+
+ def update
+ @meeting.participants_attributes = @converted_params.delete(:participants_attributes)
+ @meeting.attributes = @converted_params
+ if @meeting.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to action: 'show', id: @meeting
+ else
+ render action: 'edit'
+ end
+ end
+
+ private
+
+ def set_time_zone
+ old_time_zone = Time.zone
+ zone = User.current.time_zone
+ if zone.nil?
+ localzone = Time.now.utc_offset
+ localzone -= 3600 if Time.now.dst?
+ zone = ::ActiveSupport::TimeZone[localzone]
+ end
+ Time.zone = zone
+ yield
+ ensure
+ Time.zone = old_time_zone
+ end
+
+ def find_project
+ @project = Project.find(params[:project_id])
+ @meeting = Meeting.new
+ @meeting.project = @project
+ @meeting.author = User.current
+ end
+
+ def find_meeting
+ @meeting = Meeting
+ .includes([:project, :author, { participants: :user }, :agenda, :minutes])
+ .find(params[:id])
+ @project = @meeting.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def convert_params
+ # We do some preprocessing of `meeting_params` that we will store in this
+ # instance variable.
+ @converted_params = meeting_params
+
+ start_date = @converted_params.delete(:start_date)
+ start_time_hour = @converted_params.delete(:"start_time_hour")
+ begin
+ timestring = "#{start_date} #{start_time_hour}"
+ time = Time.zone.parse(timestring)
+ @converted_params[:start_time] = time
+ rescue ArgumentError
+ @converted_params[:start_time] = nil
+ end
+ @converted_params[:duration] = @converted_params[:duration].to_hours
+ # Force defaults on participants
+ @converted_params[:participants_attributes] ||= {}
+ @converted_params[:participants_attributes].each { |p| p.reverse_merge! attended: false, invited: false }
+ end
+
+private
+ def meeting_params
+ params.require(:meeting).permit(:title, :location, :start_time, :duration, :start_date, :start_time_hour,
+ participants_attributes: [:email, :name, :invited, :attended, :user, :user_id, :meeting, :id])
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/helpers/meeting_contents_helper.rb b/vendored-plugins/openproject-meeting/app/helpers/meeting_contents_helper.rb
new file mode 100644
index 0000000000..aeb5b6fb2d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/helpers/meeting_contents_helper.rb
@@ -0,0 +1,125 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module MeetingContentsHelper
+ def can_edit_meeting_content?(content, content_type)
+ authorize_for(content_type.pluralize, 'update') && content.editable?
+ end
+
+ def saved_meeting_content_text_present?(content)
+ !content.new_record? && content.text.present? && !content.text.empty?
+ end
+
+ def show_meeting_content_editor?(content, content_type)
+ can_edit_meeting_content?(content, content_type) && (!saved_meeting_content_text_present?(content) || content.changed?)
+ end
+
+ def meeting_content_context_menu(content, content_type)
+ menu = []
+ menu << meeting_agenda_toggle_status_link(content, content_type)
+ menu << meeting_content_edit_link(content_type) if can_edit_meeting_content?(content, content_type)
+ menu << meeting_content_history_link(content_type, content.meeting)
+ menu << meeting_content_notify_link(content_type, content.meeting) if saved_meeting_content_text_present?(content)
+ menu.join(' ')
+ end
+
+ def meeting_agenda_toggle_status_link(content, content_type)
+ content.meeting.agenda.present? && content.meeting.agenda.locked? ?
+ open_meeting_agenda_link(content_type, content.meeting) :
+ close_meeting_agenda_link(content_type, content.meeting)
+ end
+
+ def close_meeting_agenda_link(content_type, meeting)
+ case content_type
+ when 'meeting_agenda'
+ content_tag :li, '', class: 'toolbar-item' do
+ link_to_if_authorized l(:label_meeting_close),
+ { controller: '/meeting_agendas',
+ action: 'close',
+ meeting_id: meeting },
+ method: :put,
+ class: 'button icon-context icon-locked'
+ end
+ when 'meeting_minutes'
+ content_tag :li, '', class: 'toolbar-item' do
+ link_to_if_authorized l(:label_meeting_agenda_close),
+ { controller: '/meeting_agendas',
+ action: 'close',
+ meeting_id: meeting },
+ method: :put,
+ class: 'button icon-context icon-locked'
+ end
+ end
+ end
+
+ def open_meeting_agenda_link(content_type, meeting)
+ case content_type
+ when 'meeting_agenda'
+ content_tag :li, '', class: 'toolbar-item' do
+ link_to_if_authorized l(:label_meeting_open),
+ { controller: '/meeting_agendas',
+ action: 'open',
+ meeting_id: meeting },
+ method: :put,
+ class: 'button icon-context icon-unlocked',
+ confirm: l(:text_meeting_agenda_open_are_you_sure)
+ end
+ when 'meeting_minutes'
+ end
+ end
+
+ def meeting_content_edit_link(content_type)
+ content_tag :li, '', class: 'toolbar-item' do
+ content_tag :button,
+ '',
+ class: 'button button--edit-agenda',
+ onclick: "$$('.edit-#{content_type}').invoke('show');
+ $$('.show-#{content_type}').invoke('hide');
+ $$('.button--edit-agenda').invoke('addClassName', '-active');
+ $$('.button--edit-agenda').invoke('disable');
+ return false;" do
+ link_to l(:button_edit),
+ '',
+ class: 'icon-context icon-edit',
+ accesskey: accesskey(:edit)
+ end
+ end
+ end
+
+ def meeting_content_history_link(content_type, meeting)
+ content_tag :li, '', class: 'toolbar-item' do
+ link_to_if_authorized l(:label_history),
+ { controller: '/' + content_type.pluralize,
+ action: 'history',
+ meeting_id: meeting },
+ class: 'button icon-context icon-wiki'
+ end
+ end
+
+ def meeting_content_notify_link(content_type, meeting)
+ content_tag :li, '', class: 'toolbar-item' do
+ link_to_if_authorized l(:label_notify),
+ { controller: '/' + content_type.pluralize,
+ action: 'notify', meeting_id: meeting },
+ method: :put,
+ class: 'button icon-context icon-mail1'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/helpers/meetings_helper.rb b/vendored-plugins/openproject-meeting/app/helpers/meetings_helper.rb
new file mode 100644
index 0000000000..3c887bb478
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/helpers/meetings_helper.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module MeetingsHelper
+ def format_participant_list(participants)
+ participants.sort.map { |p| link_to_user p.user }.join('; ').html_safe
+ end
+
+ def render_meeting_journal(model, journal, options = {})
+ return '' if journal.initial?
+ journal_content = render_journal_details(journal, :label_updated_time_by, model, options)
+ content_tag 'div', journal_content, id: "change-#{journal.id}", class: 'journal'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/mailers/meeting_mailer.rb b/vendored-plugins/openproject-meeting/app/mailers/meeting_mailer.rb
new file mode 100644
index 0000000000..33c03be37d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/mailers/meeting_mailer.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingMailer < UserMailer
+ def content_for_review(content, content_type, address)
+ @meeting = content.meeting
+ @content_type = content_type
+
+ open_project_headers 'Project' => @meeting.project.identifier,
+ 'Meeting-Id' => @meeting.id
+
+ subject = "[#{@meeting.project.name}] #{I18n.t(:"label_#{content_type}")}: #{@meeting.title}"
+ mail to: address, subject: subject
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/activity/meeting_activity_provider.rb b/vendored-plugins/openproject-meeting/app/models/activity/meeting_activity_provider.rb
new file mode 100644
index 0000000000..811c83fe9b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/activity/meeting_activity_provider.rb
@@ -0,0 +1,139 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class Activity::MeetingActivityProvider < Activity::BaseActivityProvider
+ acts_as_activity_provider type: 'meetings',
+ activities: [:meeting, :meeting_content],
+ permission: :view_meetings
+
+ def extend_event_query(query, activity)
+ case activity
+ when :meeting_content
+ query.join(meetings_table).on(activity_journals_table(activity)[:meeting_id].eq(meetings_table[:id]))
+ join_cond = journal_table[:journable_type].eq('MeetingContent')
+ query.join(meeting_contents_table).on(journal_table[:journable_id].eq(meeting_contents_table[:id]).and(join_cond))
+ end
+ end
+
+ def event_query_projection(activity)
+ case activity
+ when :meeting
+ [
+ activity_journal_projection_statement(:title, 'meeting_title', activity),
+ activity_journal_projection_statement(:start_time, 'meeting_start_time', activity),
+ activity_journal_projection_statement(:duration, 'meeting_duration', activity),
+ activity_journal_projection_statement(:project_id, 'project_id', activity)
+ ]
+ else
+ [
+ projection_statement(meeting_contents_table, :type, 'meeting_content_type'),
+ projection_statement(meetings_table, :id, 'meeting_id'),
+ projection_statement(meetings_table, :title, 'meeting_title'),
+ projection_statement(meetings_table, :project_id, 'project_id'),
+ ]
+ end
+ end
+
+ def activitied_type(activity)
+ (activity == :meeting) ? Meeting : MeetingContent
+ end
+
+ def projects_reference_table(activity)
+ case activity
+ when :meeting
+ activity_journals_table(activity)
+ else
+ meetings_table
+ end
+ end
+
+ def activity_journals_table(activity)
+ case activity
+ when :meeting
+ @activity_journals_table = Arel::Table.new(JournalManager.journal_class(Meeting).table_name)
+ else
+ @activity_journals_table = Arel::Table.new(JournalManager.journal_class(MeetingContent).table_name)
+ end
+ end
+
+ protected
+
+ def event_name(event, activity)
+ case event['event_description']
+ when 'Agenda closed'
+ I18n.t('meeting_agenda_closed', scope: 'events')
+ when 'Agenda opened'
+ I18n.t('meeting_agenda_opened', scope: 'events')
+ when 'Minutes created'
+ I18n.t('meeting_minutes_created', scope: 'events')
+ else
+ super
+ end
+ end
+
+ def event_title(event, activity)
+ case activity
+ when :meeting
+ start_time = event['meeting_start_time'].is_a?(String) ? DateTime.parse(event['meeting_start_time'])
+ : event['meeting_start_time']
+ end_time = start_time + event['meeting_duration'].to_f.hours
+
+ "#{l :label_meeting}: #{event['meeting_title']} (#{format_date start_time} #{format_time start_time, false}-#{format_time end_time, false})"
+ else
+ "#{event['meeting_content_type'].constantize.model_name.human}: #{event['meeting_title']}"
+ end
+ end
+
+ def event_type(event, activity)
+ case activity
+ when :meeting
+ 'meeting'
+ else
+ (event['meeting_content_type'].include?('Agenda')) ? 'meeting-agenda' : 'meeting-minutes'
+ end
+ end
+
+ def event_path(event, activity)
+ id = activity_id(event, activity)
+
+ url_helpers.meeting_path(id)
+ end
+
+ def event_url(event, activity)
+ id = activity_id(event, activity)
+
+ url_helpers.meeting_url(id)
+ end
+
+ private
+
+ def meetings_table
+ @meetings_table ||= Arel::Table.new(:meetings)
+ end
+
+ def meeting_contents_table
+ @meeting_contents_table ||= Arel::Table.new(:meeting_contents)
+ end
+
+ def activity_id(event, activity)
+ (activity == :meeting) ? event['journable_id'] : event['meeting_id']
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/journal/meeting_content_journal.rb b/vendored-plugins/openproject-meeting/app/models/journal/meeting_content_journal.rb
new file mode 100644
index 0000000000..42a1db95b1
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/journal/meeting_content_journal.rb
@@ -0,0 +1,31 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class Journal::MeetingContentJournal < Journal::BaseJournal
+ self.table_name = 'meeting_content_journals'
+
+ belongs_to :meeting
+ belongs_to :author, class_name: 'User', foreign_key: 'author_id'
+
+ def editable?
+ false
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/journal/meeting_journal.rb b/vendored-plugins/openproject-meeting/app/models/journal/meeting_journal.rb
new file mode 100644
index 0000000000..c0089e7290
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/journal/meeting_journal.rb
@@ -0,0 +1,26 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class Journal::MeetingJournal < Journal::BaseJournal
+ self.table_name = 'meeting_journals'
+
+ belongs_to :author, class_name: 'User', foreign_key: 'author_id'
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/meeting.rb b/vendored-plugins/openproject-meeting/app/models/meeting.rb
new file mode 100644
index 0000000000..d0e3cd7649
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/meeting.rb
@@ -0,0 +1,188 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class Meeting < ActiveRecord::Base
+ self.table_name = 'meetings'
+
+ belongs_to :project
+ belongs_to :author, class_name: 'User', foreign_key: 'author_id'
+ has_one :agenda, dependent: :destroy, class_name: 'MeetingAgenda'
+ has_one :minutes, dependent: :destroy, class_name: 'MeetingMinutes'
+ has_many :contents, -> { readonly }, class_name: 'MeetingContent'
+ has_many :participants, dependent: :destroy, class_name: 'MeetingParticipant'
+
+ default_scope {
+ order("#{Meeting.table_name}.start_time DESC")
+ }
+ scope :from_tomorrow, -> { where(['start_time >= ?', Date.tomorrow.beginning_of_day]) }
+ scope :with_users_by_date, -> {
+ order("#{Meeting.table_name}.title ASC")
+ .includes({ participants: :user }, :author)
+ }
+
+ acts_as_watchable
+
+ acts_as_searchable columns: ["#{table_name}.title", "#{MeetingContent.table_name}.text"],
+ include: [:contents, :project],
+ references: :meeting_contents,
+ date_column: "#{table_name}.created_at"
+
+ acts_as_journalized
+ acts_as_event title: Proc.new {|o|
+ "#{l :label_meeting}: #{o.title} \
+ #{format_date o.start_time} \
+ #{format_time o.start_time, false}-#{format_time o.end_time, false})"
+ },
+ url: Proc.new { |o| { controller: '/meetings', action: 'show', id: o } },
+ author: Proc.new(&:user),
+ description: ''
+
+ register_on_journal_formatter(:plaintext, 'title')
+ register_on_journal_formatter(:fraction, 'duration')
+ register_on_journal_formatter(:datetime, 'start_time')
+ register_on_journal_formatter(:plaintext, 'location')
+
+ accepts_nested_attributes_for :participants, allow_destroy: true
+
+ validates_presence_of :title, :start_time, :duration
+
+ before_save :add_new_participants_as_watcher
+
+ after_initialize :set_initial_values
+
+ User.before_destroy do |user|
+ Meeting.where(['author_id = ?', user.id]).update_all ['author_id = ?', DeletedUser.first.id]
+ end
+
+ def start_date
+ # the text_field + calendar_for form helpers expect a Date
+ start_time.to_date if start_time.present?
+ end
+
+ def start_time_hour
+ start_time.present? ? start_time.strftime('%H:%M') : '00:00'
+ end
+
+ def start_month
+ start_time.month
+ end
+
+ def start_year
+ start_time.year
+ end
+
+ def end_time
+ start_time + duration.hours
+ end
+
+ def to_s
+ title
+ end
+
+ def text
+ agenda.text if agenda.present?
+ end
+
+ def author=(user)
+ super
+ # Don't add the author as participant if we already have some through nested attributes
+ participants.build(user: user, invited: true) if self.new_record? && participants.empty? && user
+ end
+
+ # Returns true if usr or current user is allowed to view the meeting
+ def visible?(user = nil)
+ (user || User.current).allowed_to?(:view_meetings, project)
+ end
+
+ def all_changeable_participants
+ changeable_participants = participants.select(&:invited).collect(&:user)
+ changeable_participants = changeable_participants + participants.select(&:attended).collect(&:user)
+ changeable_participants = changeable_participants + \
+ project.users.includes(memberships: [:roles, :project]).select { |u| self.visible?(u) }
+
+ changeable_participants.uniq(&:id)
+ end
+
+ def copy(attrs)
+ copy = dup
+
+ copy.author = attrs.delete(:author)
+ copy.attributes = attrs
+ copy.send(:set_initial_values)
+
+ copy.participants.clear
+ copy.participants_attributes = participants.collect(&:copy_attributes)
+
+ copy
+ end
+
+ def self.group_by_time(meetings)
+ by_start_year_month_date = ActiveSupport::OrderedHash.new do |hy, year|
+ hy[year] = ActiveSupport::OrderedHash.new do |hm, month|
+ hm[month] = ActiveSupport::OrderedHash.new
+ end
+ end
+
+ meetings.group_by(&:start_year).each do |year, objs|
+
+ objs.group_by(&:start_month).each do |month, objs|
+
+ objs.group_by(&:start_date).each do |date, objs|
+
+ by_start_year_month_date[year][month][date] = objs
+
+ end
+
+ end
+
+ end
+
+ by_start_year_month_date
+ end
+
+ def close_agenda_and_copy_to_minutes!
+ agenda.lock!
+ create_minutes(text: agenda.text, comment: 'Minutes created')
+ end
+
+ alias :original_participants_attributes= :participants_attributes=
+ def participants_attributes=(attrs)
+ attrs.each do |participant|
+ participant['_destroy'] = true if !(participant['attended'] || participant['invited'])
+ end
+ self.original_participants_attributes = attrs
+ end
+
+ protected
+
+ def set_initial_values
+ # set defaults
+ self.start_time ||= Date.tomorrow + 10.hours
+ self.duration ||= 1
+ end
+
+ private
+
+ def add_new_participants_as_watcher
+ participants.select(&:new_record?).each do |p|
+ add_watcher(p.user)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/meeting_agenda.rb b/vendored-plugins/openproject-meeting/app/models/meeting_agenda.rb
new file mode 100644
index 0000000000..acd63a122d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/meeting_agenda.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingAgenda < MeetingContent
+ # TODO: internationalize the comments
+ def lock!(user = User.current)
+ self.comment = 'Agenda closed'
+ self.author = user
+ self.locked = true
+ save
+ end
+
+ def unlock!(user = User.current)
+ self.comment = 'Agenda opened'
+ self.author = user
+ self.locked = false
+ save
+ end
+
+ def editable?
+ !locked?
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/meeting_content.rb b/vendored-plugins/openproject-meeting/app/models/meeting_content.rb
new file mode 100644
index 0000000000..b08c088fc3
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/meeting_content.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingContent < ActiveRecord::Base
+ belongs_to :meeting
+ belongs_to :author, class_name: 'User', foreign_key: 'author_id'
+
+ attr_accessor :comment
+
+ validates_length_of :comment, maximum: 255, allow_nil: true
+
+ before_save :comment_to_journal_notes
+
+ acts_as_journalized
+ acts_as_event type: Proc.new { |o| "#{o.class.to_s.underscore.dasherize}" },
+ title: Proc.new { |o| "#{o.class.model_name.human}: #{o.meeting.title}" },
+ url: Proc.new { |o| { controller: '/meetings', action: 'show', id: o.meeting } }
+
+ User.before_destroy do |user|
+ MeetingContent.where(['author_id = ?', user.id]).update_all ['author_id = ?', DeletedUser.first]
+ end
+
+ def editable?
+ true
+ end
+
+ def diff(version_to = nil, version_from = nil)
+ version_to = version_to ? version_to.to_i : version
+ version_from = version_from ? version_from.to_i : version_to - 1
+ version_to, version_from = version_from, version_to unless version_from < version_to
+
+ content_to = journals.find_by_version(version_to)
+ content_from = journals.find_by_version(version_from)
+
+ (content_to && content_from) ? WikiPage::WikiDiff.new(content_to, content_from) : nil
+ end
+
+ def at_version(version)
+ journals
+ .joins("JOIN meeting_contents ON meeting_contents.id = journals.journable_id AND meeting_contents.type='#{self.class}'")
+ .where(version: version)
+ .first.data
+ end
+
+ # Compatibility for mailer.rb
+ def updated_on
+ updated_at
+ end
+
+ # Show the project on activity and search views
+ def project
+ meeting.project
+ end
+
+ # Provided for compatibility of the old pre-journalized migration
+ def self.create_versioned_table
+ end
+
+ # Provided for compatibility of the old pre-journalized migration
+ def self.drop_versioned_table
+ end
+
+ private
+
+ def comment_to_journal_notes
+ add_journal(author, comment) unless changes.empty?
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/meeting_minutes.rb b/vendored-plugins/openproject-meeting/app/models/meeting_minutes.rb
new file mode 100644
index 0000000000..605ecc5aab
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/meeting_minutes.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingMinutes < MeetingContent
+ def editable?
+ meeting.agenda.present? && meeting.agenda.locked?
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/models/meeting_participant.rb b/vendored-plugins/openproject-meeting/app/models/meeting_participant.rb
new file mode 100644
index 0000000000..fa06951e86
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/models/meeting_participant.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MeetingParticipant < ActiveRecord::Base
+ belongs_to :meeting
+ belongs_to :user
+
+ scope :invited, -> { where(invited: true) }
+ scope :attended, -> { where(attended: true) }
+
+ User.before_destroy do |user|
+ MeetingParticipant.where(['user_id = ?', user.id]).update_all ['user_id = ?', DeletedUser.first]
+ end
+
+ def name
+ user.present? ? user.name : name
+ end
+
+ def mail
+ user.present? ? user.mail : mail
+ end
+
+ def <=>(participant)
+ to_s.downcase <=> participant.to_s.downcase
+ end
+
+ alias :to_s :name
+
+ def copy_attributes
+ # create a clean attribute set allowing to attach participants to different meetings
+ attributes.reject { |k, _v| ['id', 'meeting_id', 'attended', 'created_at', 'updated_at'].include?(k) }
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/app/views/hooks/meetings/_activity_index_head.html.erb b/vendored-plugins/openproject-meeting/app/views/hooks/meetings/_activity_index_head.html.erb
new file mode 100644
index 0000000000..aff9040c13
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/hooks/meetings/_activity_index_head.html.erb
@@ -0,0 +1,22 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= stylesheet_link_tag 'meeting/meeting.css' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_contents/_form.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_contents/_form.html.erb
new file mode 100644
index 0000000000..b6f44fa1a6
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_contents/_form.html.erb
@@ -0,0 +1,44 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= form_for content, :url => {:controller => '/' + content_type.pluralize, :action => 'update', :meeting_id => content.meeting}, :html => {:id => "#{content_type}_form", :method => :put} do |f| %>
+<%= error_messages_for content_type %>
+
+<%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit),
+ :'data-wp_autocomplete_url' => work_packages_auto_complete_path(:project_id => @project, :format => :json) %>
+<%= f.hidden_field :lock_version %>
+<%= Meeting.human_attribute_name(:comments) %> <%= f.text_field :comment, :size => 120 %>
+<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
+<% path = send("preview_#{content_type}_path", content.meeting) %>
+<%= preview_link path, "#{content_type}_form", { class: 'button preview' } %>
+<%= link_to l(:button_cancel), "#",
+ :onclick => "$$('.show-#{content_type}').invoke('show');
+ $$('.edit-#{content_type}').invoke('hide');
+ $$('.button--edit-agenda').invoke('removeClassName', '-active');
+ $$('.button--edit-agenda').invoke('enable');
+ return false;",
+ class: 'button' %>
+<%= wikitoolbar_for "#{content_type}_text" %>
+<% end %>
+
+
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_contents/_show.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_contents/_show.html.erb
new file mode 100644
index 0000000000..a3a484b243
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_contents/_show.html.erb
@@ -0,0 +1,50 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%
+ tab ||= locals[:tab_contents] if defined? locals
+ content, content_type = tab[:content], tab[:content_type] if tab && tab.present?
+-%>
+
+
+ <%= toolbar title: l(:"label_#{content_type}") do %>
+ <%=raw meeting_content_context_menu content, content_type %>
+ <% end %>
+
+
+ <% if can_edit_meeting_content?(content, content_type) -%>
+
>
+ <%= render(:partial => "meeting_contents/form", :locals => {:content => content, :content_type => content_type}) %>
+
+ <% end -%>
+
+ <% if saved_meeting_content_text_present?(content) -%>
+
+ <%= format_text(content.text, :object => @meeting) %>
+
+ <% else -%>
+
<%= l(:label_no_data) %>
+ <% end -%>
+
+ <%= javascript_tag(show_meeting_content_editor?(content, content_type) ? "$$('.show-#{content_type}').invoke('hide');" : "$$('.edit-#{content_type}').invoke('hide');") %>
+
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_contents/diff.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_contents/diff.html.erb
new file mode 100644
index 0000000000..407a789ccd
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_contents/diff.html.erb
@@ -0,0 +1,48 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title "#{l(:label_meeting_diff)}: #{@meeting.title}" %>
+<%= toolbar title: l(:"label_#{@content_type}"),
+ link_to: link_to(@meeting, @meeting) %>
+ <% if authorize_for(@content_type.pluralize, :history) %>
+
+ <%= link_to({controller: "/#{@content_type.pluralize}", action: 'history', meeting_id: @meeting }, class: 'button') do %>
+ <%= l(:label_history) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
+<%= l(:label_version) %> <%= link_to @diff.content_from.version, send(:"#{@content_type}_version_path", @meeting, @diff.content_from.version) %>
+(<%= link_to_user(@diff.content_from.user) %>, <%= format_time(@diff.content_from.created_at) %>)
+→
+<%= l(:label_version) %> <%= link_to @diff.content_to.version, send(:"#{@content_type}_version_path", @meeting, @diff.content_to.version) %>/<%= @content.version %>
+(<%= link_to_user(@diff.content_to.user) %>, <%= format_time(@diff.content_to.created_at) %>)
+
+
+
+
+
+<%= simple_format_without_paragraph @diff.to_html %>
+
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_contents/history.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_contents/history.html.erb
new file mode 100644
index 0000000000..2a9c60fa63
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_contents/history.html.erb
@@ -0,0 +1,61 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title "#{l(:label_history)}: #{@meeting.title}" %>
+
+<%= toolbar title: l(:"label_#{@content_type}"),
+ link_to: link_to(@meeting, @meeting) %>
+
+<%= l(:label_history) %>
+
+<%= form_tag({:action => "diff"}, :method => :get) do %>
+
+
+ #
+
+
+ <%= Meeting.human_attribute_name(:updated_on) %>
+ <%= Meeting.human_attribute_name(:author) %>
+ <%= Meeting.human_attribute_name(:comments) %>
+
+
+<% show_diff = @content_versions.size > 1 %>
+<% @content_versions.each_with_index do |content_version,index| %>
+">
+
+ <%= content_version.version == @content.version ?
+ link_to(content_version.version, tab_meeting_path(@meeting, :tab => @content_type.sub(/^meeting_/, ''))) :
+ link_to(content_version.version, send(:"#{@content_type}_version_path", @meeting, content_version.version)) %>
+
+ <%= radio_button_tag('version_to', content_version.version, (index==0), :id => "checkbox-from-#{index}", :onclick => "$('checkbox-to-#{index+1}').checked=true;") if show_diff && (index < @content_versions.size-1) %>
+ <%= radio_button_tag('version_from', content_version.version, (index==1), :id => "checkbox-to-#{index}") if show_diff && (index > 0) %>
+ <%= format_time(content_version.created_at) %>
+ <%= User.find content_version.user_id %>
+ <%=h content_version.notes %>
+
+<% end %>
+
+
+<%= styled_button_tag l(:label_view_diff), class: '-small -highlight' if show_diff %>
+<%= pagination_links_full @content_versions %>
+<% end %>
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_contents/show.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_contents/show.html.erb
new file mode 100644
index 0000000000..4d7a311cf7
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_contents/show.html.erb
@@ -0,0 +1,22 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= render(:partial => "meeting_contents/show", :locals => {:content => @content, :content_type => @content_type, :title => "#{l(:"label_#{@content_type}")}: #{link_to @meeting, @meeting}"}) %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.html.erb b/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.html.erb
new file mode 100644
index 0000000000..c77e54089d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.html.erb
@@ -0,0 +1,38 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= link_to(@meeting.project.name, project_url(@meeting.project)) %>: <%= link_to(@meeting.title, meeting_url(@meeting)) %>
+<%= @meeting.author %>
+
+
+<%=t :label_meeting_date_time %>: <%= format_date @meeting.start_date %> <%= format_time @meeting.start_time, false %>-<%= format_time @meeting.end_time, false %> <%= Time.zone %>
+<%=Meeting.human_attribute_name(:location) %>: <%= @meeting.location %>
+<%=Meeting.human_attribute_name(:participants_invited) %>: <%= @meeting.participants.invited.sort.join("; ") %>
+<%=Meeting.human_attribute_name(:participants_attended) %>: <%= @meeting.participants.attended.sort.join("; ") %>
+
+
+
+ <%=raw t(:"text_review_#{@content_type}",
+ :author => h(User.current),
+ :link => link_to(t(:"text_#{@content_type}_for_meeting",
+ :meeting => @meeting.title),
+ meeting_url(@meeting))) %>
+
diff --git a/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.text.erb b/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.text.erb
new file mode 100644
index 0000000000..bdc6637c86
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meeting_mailer/content_for_review.text.erb
@@ -0,0 +1,33 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= @meeting.project.name %>: <%= @meeting.title %> (<%= meeting_url(@meeting) %>)
+<%= @meeting.author %>
+
+<%=t :label_meeting_date_time %>: <%= format_date @meeting.start_date %> <%= format_time @meeting.start_time, false %>-<%= format_time @meeting.end_time, false %> <%= Time.zone %>
+<%= Meeting.human_attribute_name(:location) %>: <%= @meeting.location %>
+<%= Meeting.human_attribute_name(:participants_invited) %>: <%= @meeting.participants.invited.sort.join("; ") %>
+<%= Meeting.human_attribute_name(:participants_attended) %>: <%= @meeting.participants.attended.sort.join("; ") %>
+
+<%=t(:"text_review_#{@content_type}",
+ :author => User.current,
+ :link => t(:"text_#{@content_type}_for_meeting",
+ :meeting => @meeting.title) + " (#{meeting_url(@meeting)})") %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meetings/_form.html.erb b/vendored-plugins/openproject-meeting/app/views/meetings/_form.html.erb
new file mode 100644
index 0000000000..9282e26899
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meetings/_form.html.erb
@@ -0,0 +1,127 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%= error_messages_for 'meeting' %>
+
+
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meetings/edit.html.erb b/vendored-plugins/openproject-meeting/app/views/meetings/edit.html.erb
new file mode 100644
index 0000000000..6402fdd032
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meetings/edit.html.erb
@@ -0,0 +1,30 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title "#{l(:label_meeting_edit)}: #{@meeting.title}" %>
+
+<%= toolbar title: "#{l(:label_meeting)} ##{@meeting.id}" %>
+<%= labelled_tabular_form_for @meeting, :url => {:controller => '/meetings', :action => 'update'}, :html => {:id => 'meeting-form', :method => :put} do |f| -%>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+<%= styled_button_tag l(:button_save), class: '-highlight -with-icon icon-checkmark' %>
+<%= link_to l(:button_cancel), { :action => 'show', :id => @meeting },
+ class: 'button' %>
+<% end if @project %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meetings/index.html.erb b/vendored-plugins/openproject-meeting/app/views/meetings/index.html.erb
new file mode 100644
index 0000000000..1c7d285c0c
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meetings/index.html.erb
@@ -0,0 +1,69 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_meeting_plural) %>
+
+<%= toolbar title: l(:label_meeting_plural) do %>
+ <% if authorize_for(:meetings, :new) %>
+
+
+ <%= I18n.t(:label_meeting_new) %>
+
+ <% end %>
+<% end %>
+
+<% if @meetings_by_start_year_month_date.empty? -%>
+<%= l(:label_no_data) %>
+<% else -%>
+
+<% @meetings_by_start_year_month_date.each do |year,meetings_by_start_month_date| -%>
+<% meetings_by_start_month_date.each do |month,meetings_by_start_date| -%>
+
<%= "#{month_name(month)} #{year}" %>
+
+ <% meetings_by_start_date.each do |date,meetings| -%>
+
" class="date"><%= format_activity_day(date) %>
+
+ <% meetings.each do |meeting| -%>
+
+ <%= avatar meeting.author, :size => "24" %>
+ <%= format_time meeting.start_time, false %>-<%= format_time meeting.end_time, false %>
+ <%= link_to h(meeting.title), :controller => '/meetings', :action => 'show', :id => meeting %>
+
+
+ <%= Meeting.human_attribute_name(:location) %> : <%=h meeting.location %>
+ <%= Meeting.human_attribute_name(:participants_invited) %> (<%= meeting.participants.select(&:invited).count %>): <%= format_participant_list meeting.participants.select(&:invited) %>
+ <%= Meeting.human_attribute_name(:participants_attended) %> (<%= meeting.participants.select(&:attended).count %>): <%= format_participant_list meeting.participants.select(&:attended) %>
+
+ <% end -%>
+
+ <% end -%>
+
+<% end -%>
+<% end -%>
+
+<% end -%>
+
+
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meetings/new.html.erb b/vendored-plugins/openproject-meeting/app/views/meetings/new.html.erb
new file mode 100644
index 0000000000..aa1320d891
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meetings/new.html.erb
@@ -0,0 +1,29 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_meeting_new) %>
+<%= toolbar title: l(:label_meeting_new) %>
+<%= labelled_tabular_form_for @meeting, :url => {:controller => '/meetings', :action => 'create', :project_id => @project}, :html => {:id => 'meeting-form'} do |f| -%>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= styled_button_tag l(:button_create), class: '-highlight' %>
+ <%= link_to l(:button_cancel), { :action => 'index', :project_id => @project },
+ class: 'button' %>
+<% end if @project %>
diff --git a/vendored-plugins/openproject-meeting/app/views/meetings/show.html.erb b/vendored-plugins/openproject-meeting/app/views/meetings/show.html.erb
new file mode 100644
index 0000000000..5f63990223
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/meetings/show.html.erb
@@ -0,0 +1,92 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title "#{l(:label_meeting)}: #{@meeting.title}" %>
+<%= toolbar title: l(:label_meeting),
+ link_to: link_to(@meeting),
+ html: { class: 'meeting--main-toolbar' } do %>
+ <% unless User.current.anonymous? %>
+
+
+ <%= watcher_link @meeting, User.current %>
+
+
+ <% end %>
+ <% if authorize_for(:meetings, :edit) %>
+
+ <%= link_to({:controller => '/meetings', :action => 'edit', :id => @meeting}, class: 'button',:accesskey => accesskey(:edit)) do%>
+ <%= l(:button_edit) %>
+ <% end %>
+
+ <% end %>
+ <% if authorize_for(:meetings, :copy) %>
+
+ <%= link_to({:controller => '/meetings', :action => 'copy', :id => @meeting}, class: 'button') do %>
+ <%= l(:button_copy) %>
+ <% end %>
+
+ <% end %>
+ <% if authorize_for(:meetings, :destroy) %>
+
+ <%= link_to({controller: '/meetings', action: 'destroy', id: @meeting},
+ class: 'button',
+ method: :delete,
+ confirm: l(:text_are_you_sure)) do %>
+ <%= l(:button_delete) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
+
+
+ <%= avatar(@meeting.author) %>
+
<%= authoring @meeting.created_at, @meeting.author %>
+
+
+
<%= Meeting.human_attribute_name(:start_time) %> : <%= format_date @meeting.start_date %> <%= format_time @meeting.start_time, false %> - <%= format_time @meeting.end_time, false %> <%= Time.zone %>
+
+
+
<%= Meeting.human_attribute_name(:location) %> : <%=h @meeting.location %>
+
+
+
<%= Meeting.human_attribute_name(:participants_invited) %> : <%= format_participant_list @meeting.participants.invited %>
+
+
+
<%= Meeting.human_attribute_name(:participants_attended) %> : <%= format_participant_list @meeting.participants.attended %>
+
+
+
+
+<%= render_tabs [{:name => 'agenda', :action => :create_meeting_agendas, :partial => 'meeting_contents/show', :label => :label_meeting_agenda, :content => @meeting.agenda || @meeting.build_agenda, :content_type => "meeting_agenda"},
+ {:name => 'minutes', :action => :create_meeting_minutes, :partial => 'meeting_contents/show', :label => :label_meeting_minutes, :content => @meeting.minutes || @meeting.build_minutes, :content_type => "meeting_minutes"}] %>
+
+<% if @meeting.journals.changing.present? %>
+
+
<%=l(:label_history)%>
+ <% @meeting.journals.each do |journal| %>
+ <%= render_meeting_journal @meeting, journal %>
+ <% end %>
+
+<% end %>
+
+<%= render :partial => 'shared/meeting_header' %>
diff --git a/vendored-plugins/openproject-meeting/app/views/shared/_meeting_header.html.erb b/vendored-plugins/openproject-meeting/app/views/shared/_meeting_header.html.erb
new file mode 100644
index 0000000000..24d888014c
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/app/views/shared/_meeting_header.html.erb
@@ -0,0 +1,24 @@
+<%#-- copyright
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'meeting/meeting.css' %>
+<% end %>
diff --git a/vendored-plugins/openproject-meeting/config/locales/da.yml b/vendored-plugins/openproject-meeting/config/locales/da.yml
new file mode 100644
index 0000000000..c9ee190910
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/da.yml
@@ -0,0 +1,54 @@
+da:
+ activerecord:
+ attributes:
+ meeting:
+ location: Placering
+ duration: Varighed
+ participants: Deltagere
+ participants_attended: Tilsluttede
+ participants_invited: Inviterede
+ start_time: Tidspunkt
+ models:
+ meeting_agenda: Dagsorden
+ meeting_minutes: Referat
+ description_attended: tilsluttede
+ description_invite: inviteret
+ events:
+ meeting: Mødet er ændret
+ meeting_agenda: Mødedagsorden er ændret
+ meeting_agenda_closed: Mødedagsorden er afsluttet
+ meeting_agenda_opened: Mødedagsorden er påbegyndt
+ meeting_minutes: Mødereferat er ændret
+ meeting_minutes_created: Mødereferat er udfærdiget
+ error_notification_with_errors: 'Kunne ikke sende påmindelse. Følgende modtagere blev ikke nået: %{recipients}'
+ label_meeting: Møde
+ label_meeting_plural: Møder
+ label_meeting_new: Nyt møde
+ label_meeting_edit: Rediger møde
+ label_meeting_agenda: Dagsorden
+ label_meeting_minutes: Referat
+ label_meeting_close: Luk
+ label_meeting_open: Åbn
+ label_meeting_agenda_close: Afslut dagsorden og påbegynd referat
+ label_meeting_date_time: Dato/tidspunkt
+ label_meeting_diff: Uenigheder
+ label_notify: Send til vurdering
+ label_version: Version
+ notice_successful_notification: Påmindelse er afsendt
+ notice_timezone_missing: 'Der er ikke sat en tidszone og systemet har valgt %{zone}. For at vælge din egen tidszone, klik venligst her.'
+ permission_create_meetings: Opret møder
+ permission_edit_meetings: Rediger møder
+ permission_delete_meetings: Slet møder
+ permission_view_meetings: Se møder
+ permission_create_meeting_agendas: Håndter dagsordner
+ permission_close_meeting_agendas: Luk dagsordner
+ permission_send_meeting_agendas_notification: Send påmindelse om vurdering af dagsorden
+ permission_create_meeting_minutes: Håndter referater
+ permission_send_meeting_minutes_notification: Send påmindelse om vurdering af referater
+ project_module_meetings: Møder
+ text_in_hours: i timer
+ text_meeting_agenda_for_meeting: 'dagsorden for mødet "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Indhold i referatet, der ikke er gemt vil gå tabt! Fortsæt?
+ text_meeting_minutes_for_meeting: 'referat for mødet "%{meeting}"'
+ text_review_meeting_agenda: '%{author} har lagt dette %{link} ud til vurdering.'
+ text_review_meeting_minutes: '%{author} har lagt dette %{link} ud til vurdering.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/de.yml b/vendored-plugins/openproject-meeting/config/locales/de.yml
new file mode 100644
index 0000000000..7e23470882
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/de.yml
@@ -0,0 +1,54 @@
+de:
+ activerecord:
+ attributes:
+ meeting:
+ location: Ort/Raum
+ duration: Dauer
+ participants: Teilnehmer
+ participants_attended: Anwesend
+ participants_invited: Eingeladen
+ start_time: Anfangszeit
+ models:
+ meeting_agenda: Agenda
+ meeting_minutes: Protokoll
+ description_attended: teilgenommen
+ description_invite: eingeladen
+ events:
+ meeting: Besprechung bearbeitet
+ meeting_agenda: Agenda bearbeitet
+ meeting_agenda_closed: Agenda abgeschlossen
+ meeting_agenda_opened: Agenda geöffnet
+ meeting_minutes: Protokoll bearbeitet
+ meeting_minutes_created: Protokoll erstellt
+ error_notification_with_errors: 'Benachrichtigungversenden fehlgeschlagen. Folgende Empfänger konnten nicht benachrichtigt werden: %{recipients}'
+ label_meeting: Besprechung
+ label_meeting_plural: Besprechungen
+ label_meeting_new: Neue Besprechung
+ label_meeting_edit: Besprechung bearbeiten
+ label_meeting_agenda: Agenda
+ label_meeting_minutes: Protokoll
+ label_meeting_close: Schließen
+ label_meeting_open: Öffnen
+ label_meeting_agenda_close: Agenda schließen um mit dem Protokoll zu beginnen
+ label_meeting_date_time: Datum/Uhrzeit
+ label_meeting_diff: Differenz
+ label_notify: Zur Einsicht verschicken
+ label_version: Version
+ notice_successful_notification: Benachrichtigung erfolgreich gesendet
+ notice_timezone_missing: 'Keine Zeitzone eingestellt und daher %{zone} angenommen. Um Ihre Zeitzone einzustellen, klicken Sie bitte hier.'
+ permission_create_meetings: Besprechungen erstellen
+ permission_edit_meetings: Besprechungen bearbeiten
+ permission_delete_meetings: Besprechungen löschen
+ permission_view_meetings: Besprechungen ansehen
+ permission_create_meeting_agendas: Agenden anlegen/bearbeiten
+ permission_close_meeting_agendas: Agenden schließen
+ permission_send_meeting_agendas_notification: Benachrichtigungen für Agenden verschicken
+ permission_create_meeting_minutes: Protokolle anlegen/bearbeiten
+ permission_send_meeting_minutes_notification: Benachrichtigungen für Protokolle verschicken
+ project_module_meetings: Besprechungen
+ text_in_hours: in Stunden
+ text_meeting_agenda_for_meeting: 'Agenda für die Besprechung "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Nicht-gespeicherte Inhalte des Protokolls werden durch diese Aktion verworfen! Weitermachen?
+ text_meeting_minutes_for_meeting: 'Protokoll für die Besprechung "%{meeting}"'
+ text_review_meeting_agenda: '%{author} hat die %{link} zur Einsicht freigegeben.'
+ text_review_meeting_minutes: '%{author} hat die %{link} zur Einsicht freigegeben.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/en.yml b/vendored-plugins/openproject-meeting/config/locales/en.yml
new file mode 100644
index 0000000000..109a8379cf
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/en.yml
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+# English strings go here for Rails i18n
+en:
+ activerecord:
+ attributes:
+ meeting:
+ location: "Location"
+ duration: "Duration"
+ participants: "Participants"
+ participants_attended: "Attendees"
+ participants_invited: "Invitees"
+ start_time: "Time"
+ models:
+ meeting_agenda: "Agenda"
+ meeting_minutes: "Minutes"
+
+ description_attended: "attended"
+ description_invite: "invited"
+
+ events:
+ meeting: Meeting edited
+ meeting_agenda: Meeting agenda edited
+ meeting_agenda_closed: Meeting agenda closed
+ meeting_agenda_opened: Meeting agenda opened
+ meeting_minutes: Meeting minutes edited
+ meeting_minutes_created: Meeting minutes created
+
+ error_notification_with_errors: "Failed to send notification. The following recipients could not be notified: %{recipients}"
+
+ label_meeting: "Meeting"
+ label_meeting_plural: "Meetings"
+ label_meeting_new: "New Meeting"
+ label_meeting_edit: "Edit Meeting"
+ label_meeting_agenda: "Agenda"
+ label_meeting_minutes: "Minutes"
+ label_meeting_close: "Close"
+ label_meeting_open: "Open"
+ label_meeting_agenda_close: "Close the agenda to begin the Minutes"
+ label_meeting_date_time: "Date/Time"
+ label_meeting_diff: "Diff"
+ label_notify: "Send for review"
+ label_version: "Version"
+ label_time_zone: "Time zone"
+ label_start_date: "Start date"
+
+ notice_successful_notification: "Notification sent successfully"
+ notice_timezone_missing: No time zone is set and %{zone} is assumed. To choose your time zone, please click here.
+
+ permission_create_meetings: "Create meetings"
+ permission_edit_meetings: "Edit meetings"
+ permission_delete_meetings: "Delete meetings"
+ permission_view_meetings: "View meetings"
+ permission_create_meeting_agendas: "Manage agendas"
+ permission_close_meeting_agendas: "Close agendas"
+ permission_send_meeting_agendas_notification: "Send review notification for agendas"
+ permission_create_meeting_minutes: "Manage minutes"
+ permission_send_meeting_minutes_notification: "Send review notification for minutes"
+
+ project_module_meetings: "Meetings"
+
+ text_in_hours: "in hours"
+ text_meeting_agenda_for_meeting: 'agenda for the meeting "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: "Unsaved content in the minutes will be lost! Continue?"
+ text_meeting_minutes_for_meeting: 'minutes for the meeting "%{meeting}"'
+ text_review_meeting_agenda: "%{author} has put the %{link} up for review."
+ text_review_meeting_minutes: "%{author} has put the %{link} up for review."
diff --git a/vendored-plugins/openproject-meeting/config/locales/fr.yml b/vendored-plugins/openproject-meeting/config/locales/fr.yml
new file mode 100644
index 0000000000..1c09004426
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/fr.yml
@@ -0,0 +1,54 @@
+fr:
+ activerecord:
+ attributes:
+ meeting:
+ location: Lieu
+ duration: Durée
+ participants: Participant(s)
+ participants_attended: Présent(s)
+ participants_invited: Invités
+ start_time: Heure
+ models:
+ meeting_agenda: Ordre du jour
+ meeting_minutes: Compte-rendu
+ description_attended: ont participé
+ description_invite: invité
+ events:
+ meeting: Réunion modifiée
+ meeting_agenda: Ordre du jour de la réunion modifié
+ meeting_agenda_closed: Ordre du jour de la réunion fermé
+ meeting_agenda_opened: Ordre du jour de la réunion ouvert
+ meeting_minutes: Compte-rendu de la réunion modifié
+ meeting_minutes_created: Compte-rendu de la réunion créé
+ error_notification_with_errors: "L'envoi de notifications a échoué. Les destinataires suivant n'ont pas pu être notifiés : %{recipients}"
+ label_meeting: Réunion
+ label_meeting_plural: Réunions
+ label_meeting_new: Nouvelle réunion
+ label_meeting_edit: Modifier la réunion
+ label_meeting_agenda: Ordre du jour
+ label_meeting_minutes: Compte-rendu
+ label_meeting_close: Fermer
+ label_meeting_open: Ouvrir
+ label_meeting_agenda_close: "Fermer l'ordre du jour pour commencer les procès-verbaux"
+ label_meeting_date_time: Date/Heure
+ label_meeting_diff: Différence
+ label_notify: Envoyer pour révision
+ label_version: Version
+ notice_successful_notification: Notification envoyée avec succès
+ notice_timezone_missing: "Aucun fuseau horaire n'est défini et %{zone} est supposé. Pour choisir votre fuseau horaire, veuillez cliquer ici."
+ permission_create_meetings: Créer des réunions
+ permission_edit_meetings: Modifier les réunions
+ permission_delete_meetings: Supprimer des réunions
+ permission_view_meetings: Afficher les réunions
+ permission_create_meeting_agendas: Gérer les ordres du jour
+ permission_close_meeting_agendas: Fermer les ordres du jour
+ permission_send_meeting_agendas_notification: Envoyer une notification de révision pour les ordres du jour
+ permission_create_meeting_minutes: Gérer les minutes
+ permission_send_meeting_minutes_notification: Envoyer une notification de révision pour les minutes
+ project_module_meetings: Réunions
+ text_in_hours: en heures
+ text_meeting_agenda_for_meeting: 'ordre du jour de la réunion «%{meeting} »'
+ text_meeting_agenda_open_are_you_sure: Le contenu non enregistré des minutes de la réunion sera perdu ! Continuer ?
+ text_meeting_minutes_for_meeting: 'Minutes de la réunion "%{meeting}"'
+ text_review_meeting_agenda: '%{author} à marqué le %{link} pour vérification.'
+ text_review_meeting_minutes: '%{author} à marqué le %{link} pour vérification.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/he.yml b/vendored-plugins/openproject-meeting/config/locales/he.yml
new file mode 100644
index 0000000000..63581c6289
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/he.yml
@@ -0,0 +1,54 @@
+he:
+ activerecord:
+ attributes:
+ meeting:
+ location: מיקום
+ duration: משך זמן
+ participants: משתתפים
+ participants_attended: נוכחים
+ participants_invited: מוזמנים
+ start_time: זמן
+ models:
+ meeting_agenda: סדר היום
+ meeting_minutes: דקות
+ description_attended: נכחו
+ description_invite: הוזמנו
+ events:
+ meeting: הפגישה נערכה
+ meeting_agenda: סדר היום לפגישה נערך
+ meeting_agenda_closed: סדר היום לפגישה נסגר
+ meeting_agenda_opened: סדר היום לפגישה נפתח
+ meeting_minutes: דקות פגישה נערך
+ meeting_minutes_created: דקות פגישה נוצר
+ error_notification_with_errors: 'שליחת ההודעה נכשלה. הנמענים הבאים לא יקבלו את ההודעה: %{recipients}'
+ label_meeting: פגישה
+ label_meeting_plural: פגישות
+ label_meeting_new: פגישה חדשה
+ label_meeting_edit: עריכת פגישה
+ label_meeting_agenda: סדר היום
+ label_meeting_minutes: דקות
+ label_meeting_close: סגור
+ label_meeting_open: פתח
+ label_meeting_agenda_close: סגור את סדר היום כדי להתחיל את הדקות
+ label_meeting_date_time: תאריך/שעה
+ label_meeting_diff: Diff
+ label_notify: שלח לבדיקה
+ label_version: גירסה
+ notice_successful_notification: הודעה נשלחה בהצלחה
+ notice_timezone_missing: 'אין איזור זמן מוגדר, ההערכה היא %{zone}. כדי לבחור את איזור הזמן שלך, אנא לחץ כאן.'
+ permission_create_meetings: צור פגישות
+ permission_edit_meetings: ערוך פגישות
+ permission_delete_meetings: מחק פגישות
+ permission_view_meetings: הצג פגישות
+ permission_create_meeting_agendas: נהל סדר יום
+ permission_close_meeting_agendas: סגור סדר יום
+ permission_send_meeting_agendas_notification: שלח הודעה סקירה עבור סדר יום
+ permission_create_meeting_minutes: נהל דקות
+ permission_send_meeting_minutes_notification: שלח הודעה סקירה עבור דקות
+ project_module_meetings: פגישות
+ text_in_hours: בשעות
+ text_meeting_agenda_for_meeting: 'סדר היום לפגישה "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: תוכן שלא נשמר בדקות יאבד! האם להמשיך?
+ text_meeting_minutes_for_meeting: 'דקות לפגישה "%{meeting}"'
+ text_review_meeting_agenda: 'על %{author} לשים את %{link} לבדיקה.'
+ text_review_meeting_minutes: 'על %{author} לשים את %{link} לבדיקה.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/it.yml b/vendored-plugins/openproject-meeting/config/locales/it.yml
new file mode 100644
index 0000000000..7da8ba1922
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/it.yml
@@ -0,0 +1,55 @@
+it:
+ activerecord:
+ attributes:
+ meeting:
+ location: Luogo
+ duration: Durata
+ participants: Partecipanti
+ participants_attended: Invitati
+ participants_invited: Invitati
+ start_time: Tempo
+ models:
+ meeting_agenda: Ordine del giorno
+ meeting_minutes: Verbali
+ description_attended: ha partecipato
+ description_invite: invitato
+ events:
+ meeting: Riunione modificata
+ meeting_agenda: Agenda riunione modificata
+ meeting_agenda_closed: Agenda riunione chiusa
+ meeting_agenda_opened: Agenda riunione aperta
+ meeting_minutes: Verbali riunione modificati
+ meeting_minutes_created: Verbale riunione creato
+ error_notification_with_errors: 'Impossibile inviare notifica. Non è possibile notificare i seguenti destinatari: %{recipients}'
+ label_meeting: Riunione
+ label_meeting_plural: Riunioni
+ label_meeting_new: Nuova riunione
+ label_meeting_edit: Modifica riunione
+ label_meeting_agenda: Ordine del giorno
+ label_meeting_minutes: Verbali
+ label_meeting_close: Chiuso
+ label_meeting_open: Aperto
+ label_meeting_agenda_close: "Chiudere l'agenda per iniziare i verbali"
+ label_meeting_date_time: Data/ora
+ label_meeting_diff: Differenza
+ label_notify: Invia per revisione
+ label_version: Versione
+ notice_successful_notification: Notifica inviata con successo
+ notice_timezone_missing: 'Nessun fuso orario è impostato e la %{zone} è un requisito necessario. Per scegliere il tuo fuso orario, fare clic qui.'
+ permission_create_meetings: Creare riunioni
+ permission_edit_meetings: Modificare riunioni
+ permission_delete_meetings: Annullare riunioni
+ permission_view_meetings: Vedere riunioni
+ permission_create_meeting_agendas: Gestire gli ordini del giorno
+ permission_close_meeting_agendas: Chiudere gli ordini del giorno
+ permission_send_meeting_agendas_notification: Inviare notifiche di modifica degli ordini del giorno
+ permission_create_meeting_minutes: Gestire i verbali
+ permission_send_meeting_minutes_notification: Inviare notifiche di verbali modificati
+ project_module_meetings: Riunioni
+ text_in_hours: in ore
+ text_meeting_agenda_for_meeting: 'ordine del giorno della riunione "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Il contenuto non salvato nei verbali verrà perso! Continuare?
+ text_meeting_minutes_for_meeting: |-
+ verbali per l'incontro "%{meeting}"
+ text_review_meeting_agenda: '%{author} ha pubblicato %{link} per la revisione.'
+ text_review_meeting_minutes: '%{author} ha pubblicato %{link} per la revisione.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/nl.yml b/vendored-plugins/openproject-meeting/config/locales/nl.yml
new file mode 100644
index 0000000000..2ebaa53e05
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/nl.yml
@@ -0,0 +1,54 @@
+nl:
+ activerecord:
+ attributes:
+ meeting:
+ location: Locatie
+ duration: Duur
+ participants: Deelnemers
+ participants_attended: Aanwezigen
+ participants_invited: Genodigden
+ start_time: Tijd
+ models:
+ meeting_agenda: Agenda
+ meeting_minutes: Minuten
+ description_attended: bijgewoond
+ description_invite: uitgenodigd
+ events:
+ meeting: Vergadering bewerkt
+ meeting_agenda: Vergaderagenda bewerkt
+ meeting_agenda_closed: Vergaderagenda gesloten
+ meeting_agenda_opened: Vergaderagenda geopend
+ meeting_minutes: Notulen van de vergadering bewerkt
+ meeting_minutes_created: Notulen gemaakt
+ error_notification_with_errors: 'Notificatie verzenden mislukt. De volgende geadresseerden konden niet worden gemeld: %{recipients}'
+ label_meeting: Vergadering
+ label_meeting_plural: Vergaderingen
+ label_meeting_new: Nieuwe vergadering
+ label_meeting_edit: Vergadering bewerken
+ label_meeting_agenda: Agenda
+ label_meeting_minutes: Minuten
+ label_meeting_close: Sluiten
+ label_meeting_open: Open
+ label_meeting_agenda_close: Sluit de agenda om de notulen te beginnen
+ label_meeting_date_time: Datum/Tijd
+ label_meeting_diff: Diff
+ label_notify: Verzenden voor revisie
+ label_version: Versie
+ notice_successful_notification: Notificatie succesvol verzonden
+ notice_timezone_missing: 'Geen tijdzone is ingesteld en %{zone} is aangenomen. Om uw tijdzone te kiezen, klik dan hier.'
+ permission_create_meetings: Creëer vergaderingen
+ permission_edit_meetings: Vergaderingen bewerken
+ permission_delete_meetings: Verwijder vergaderingen
+ permission_view_meetings: Bekijk vergaderingen
+ permission_create_meeting_agendas: "Agenda's beheren"
+ permission_close_meeting_agendas: "Sluit agenda's"
+ permission_send_meeting_agendas_notification: Send review notification for agendas
+ permission_create_meeting_minutes: Manage minutes
+ permission_send_meeting_minutes_notification: Send review notification for minutes
+ project_module_meetings: Vergaderingen
+ text_in_hours: in uren
+ text_meeting_agenda_for_meeting: 'agenda voor de vergadering "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Niet-opgeslagen inhoud in de notulen zullen verloren gaan! Blijven?
+ text_meeting_minutes_for_meeting: 'minuten voor de vergadering "%{meeting}"'
+ text_review_meeting_agenda: '%{author} heeft de %{link} geselecteerd voor herziening.'
+ text_review_meeting_minutes: '%{author} heeft de %{link} geselecteerd voor herziening.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/no.yml b/vendored-plugins/openproject-meeting/config/locales/no.yml
new file mode 100644
index 0000000000..d97c336c03
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/no.yml
@@ -0,0 +1,54 @@
+"no":
+ activerecord:
+ attributes:
+ meeting:
+ location: Sted
+ duration: Varighet
+ participants: Deltagere
+ participants_attended: Tilsluttete
+ participants_invited: Inviterte
+ start_time: Tidspunkt
+ models:
+ meeting_agenda: Saksliste
+ meeting_minutes: Referat
+ description_attended: deltok
+ description_invite: invitert
+ events:
+ meeting: Møtet endret
+ meeting_agenda: Saksliste endret
+ meeting_agenda_closed: Saksliste ferdiggjort
+ meeting_agenda_opened: Saksliste opprettet
+ meeting_minutes: Møtereferater endret
+ meeting_minutes_created: Møtereferater opprettet
+ error_notification_with_errors: 'Kan ikke sende påminning. Følgende mottakere kan ikke varsles: %{recipients}'
+ label_meeting: Møte
+ label_meeting_plural: Møter
+ label_meeting_new: Nytt møte
+ label_meeting_edit: Redigere møte
+ label_meeting_agenda: Saksliste
+ label_meeting_minutes: Referat
+ label_meeting_close: Afslutt
+ label_meeting_open: Åpne
+ label_meeting_agenda_close: Lukk sakslisten for å begynne referatet
+ label_meeting_date_time: Dato/klokkeslett
+ label_meeting_diff: Forskjell
+ label_notify: Send till gjennomgang
+ label_version: Versjon
+ notice_successful_notification: Påminning sendt
+ notice_timezone_missing: 'Ingen tidssone angis og %{zone} antas. Vennligst klikk her for å velge egen tidssone.'
+ permission_create_meetings: Opprett møter
+ permission_edit_meetings: Rediger møter
+ permission_delete_meetings: Slett møter
+ permission_view_meetings: Vis møter
+ permission_create_meeting_agendas: Håndter sakslister
+ permission_close_meeting_agendas: Lukk sakslister
+ permission_send_meeting_agendas_notification: Send påminning om gjennomgang av sakslister
+ permission_create_meeting_minutes: Håndter referater
+ permission_send_meeting_minutes_notification: Send påminning om gjennomgang av referater
+ project_module_meetings: Møter
+ text_in_hours: i timer
+ text_meeting_agenda_for_meeting: 'saksliste for møtet "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Ulagret innhold i refarat går tapt! Vil du fortsette?
+ text_meeting_minutes_for_meeting: 'referat for møtet "%{meeting}"'
+ text_review_meeting_agenda: '%{author} har lagt opp %{link} for gjennomgang.'
+ text_review_meeting_minutes: '%{author} har lagt opp %{link} for gjennomgang.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/pl.yml b/vendored-plugins/openproject-meeting/config/locales/pl.yml
new file mode 100644
index 0000000000..bb34b1e0de
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/pl.yml
@@ -0,0 +1,54 @@
+pl:
+ activerecord:
+ attributes:
+ meeting:
+ location: Lokalizacja
+ duration: Czas trwania
+ participants: Uczestnicy
+ participants_attended: Uczestnicy
+ participants_invited: Zaproszone osoby
+ start_time: Czas
+ models:
+ meeting_agenda: Plan spotkania
+ meeting_minutes: Minuty
+ description_attended: Obecny
+ description_invite: Zaproszony
+ events:
+ meeting: Spotkanie zmienione
+ meeting_agenda: Zmieniono plan spotkania
+ meeting_agenda_closed: Zamknięto plan spotkania
+ meeting_agenda_opened: Otwarto plan spotkania
+ meeting_minutes: Meeting minutes edited
+ meeting_minutes_created: Meeting minutes created
+ error_notification_with_errors: 'Nie udało się wysłać powiadomienie do adresatów: %{recipients}'
+ label_meeting: Spotkanie
+ label_meeting_plural: Spotkania
+ label_meeting_new: Nowe spotkanie
+ label_meeting_edit: Edycja spotkania
+ label_meeting_agenda: Plan spotkania
+ label_meeting_minutes: Minuty
+ label_meeting_close: Zamknij
+ label_meeting_open: Otwórz
+ label_meeting_agenda_close: Close the agenda to begin the Minutes
+ label_meeting_date_time: Data/Czas
+ label_meeting_diff: Różnice
+ label_notify: Wyślij do przeglądu
+ label_version: Wersja
+ notice_successful_notification: Powiadomienia wysłane pomyślnie
+ notice_timezone_missing: 'No time zone is set and %{zone} is assumed. To choose your time zone, please click here.'
+ permission_create_meetings: Utwórz spotkanie
+ permission_edit_meetings: Modyfikuj spotkania
+ permission_delete_meetings: Usuń spotkania
+ permission_view_meetings: Zobacz spotkania
+ permission_create_meeting_agendas: Zarządzaj planami
+ permission_close_meeting_agendas: Zamknij plany
+ permission_send_meeting_agendas_notification: Wyślij powiadomienie o przeglądzie planów
+ permission_create_meeting_minutes: Zarządzaj minutami
+ permission_send_meeting_minutes_notification: Send review notification for minutes
+ project_module_meetings: Spotkania
+ text_in_hours: in hours
+ text_meeting_agenda_for_meeting: 'agenda for the meeting "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Unsaved content in the minutes will be lost! Continue?
+ text_meeting_minutes_for_meeting: 'minutes for the meeting "%{meeting}"'
+ text_review_meeting_agenda: '%{author} has put the %{link} up for review.'
+ text_review_meeting_minutes: '%{author} has put the %{link} up for review.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/pt-BR.yml b/vendored-plugins/openproject-meeting/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..ca3110199e
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/pt-BR.yml
@@ -0,0 +1,54 @@
+pt-BR:
+ activerecord:
+ attributes:
+ meeting:
+ location: Local
+ duration: Duração
+ participants: Participantes
+ participants_attended: Presentes
+ participants_invited: Convidados
+ start_time: Horário
+ models:
+ meeting_agenda: Agenda
+ meeting_minutes: Atas
+ description_attended: compareceu
+ description_invite: convidado
+ events:
+ meeting: Reunião editada
+ meeting_agenda: Agenda da reunião editada
+ meeting_agenda_closed: Agenda da reunião fechada
+ meeting_agenda_opened: Agenda da reunião aberta
+ meeting_minutes: Ata da reunião editada
+ meeting_minutes_created: Ata da reunião criada
+ error_notification_with_errors: 'Falha ao enviar notificação. Os seguintes destinatários não puderam ser notificados: %{recipients}'
+ label_meeting: Reunião
+ label_meeting_plural: Reuniões
+ label_meeting_new: Nova Reunião
+ label_meeting_edit: Editar Reunião
+ label_meeting_agenda: Agenda
+ label_meeting_minutes: Atas
+ label_meeting_close: Fechar
+ label_meeting_open: Abrir
+ label_meeting_agenda_close: Fechar a agenda para iniciar a Ata
+ label_meeting_date_time: Data/Horário
+ label_meeting_diff: Diferenças
+ label_notify: Enviar para revisão
+ label_version: Versão
+ notice_successful_notification: Notificação enviada com sucesso
+ notice_timezone_missing: 'Nenhum fuso horário está definido, portanto assumiu-se %{zone}. Para escolher o seu fuso horário, clique aqui.'
+ permission_create_meetings: Criar reuniões
+ permission_edit_meetings: Editar reuniões
+ permission_delete_meetings: Excluir reuniões
+ permission_view_meetings: Visualizar reuniões
+ permission_create_meeting_agendas: Gerenciar agendas
+ permission_close_meeting_agendas: Fechar agendas
+ permission_send_meeting_agendas_notification: Enviar notificação de revisão para agendas
+ permission_create_meeting_minutes: Gerenciar atas
+ permission_send_meeting_minutes_notification: Enviar notificação de revisão das atas
+ project_module_meetings: Reuniões
+ text_in_hours: em horas
+ text_meeting_agenda_for_meeting: 'agenda para a reunião "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Conteúdo não salvo na ata será perdido! Continuar?
+ text_meeting_minutes_for_meeting: 'ata da reunião "%{meeting}"'
+ text_review_meeting_agenda: '%{author} marcou o %{link} para revisão.'
+ text_review_meeting_minutes: '%{author} marcou o %{link} para revisão.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/ru.yml b/vendored-plugins/openproject-meeting/config/locales/ru.yml
new file mode 100644
index 0000000000..1548df2471
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/ru.yml
@@ -0,0 +1,54 @@
+ru:
+ activerecord:
+ attributes:
+ meeting:
+ location: Расположение
+ duration: Длительность
+ participants: Участники
+ participants_attended: Участники
+ participants_invited: Приглашенные
+ start_time: Время
+ models:
+ meeting_agenda: Повестка дня
+ meeting_minutes: Протокол(-ы)
+ description_attended: участие
+ description_invite: приглашено
+ events:
+ meeting: Встреча отредактирована
+ meeting_agenda: Повестка встречи отредактирована
+ meeting_agenda_closed: Повестка дня встречи закрыта
+ meeting_agenda_opened: Повестка дня встречи открыта
+ meeting_minutes: 'Править протокол заседания'
+ meeting_minutes_created: Создан протокол заседания
+ error_notification_with_errors: 'Не удалось отправить уведомление. Следующие получатели не могут быть уведомлены: %{recipients}'
+ label_meeting: Совещание
+ label_meeting_plural: Совещания
+ label_meeting_new: Новое совещание
+ label_meeting_edit: Измененить совещание
+ label_meeting_agenda: Повестка дня
+ label_meeting_minutes: Протокол(-ы)
+ label_meeting_close: Закрыть
+ label_meeting_open: Открыть
+ label_meeting_agenda_close: Закрыть повестку для начала протоколирования
+ label_meeting_date_time: Дата/Время
+ label_meeting_diff: Различия
+ label_notify: Отправка на рецензию
+ label_version: Версия
+ notice_successful_notification: Уведомление успешно отправленно
+ notice_timezone_missing: 'Не установлен часовой пояс и применена %{zone}. Чтобы выбрать часовой пояс, пожалуйста, нажмите сюда.'
+ permission_create_meetings: Создание совещания
+ permission_edit_meetings: Править встречи
+ permission_delete_meetings: Удалить встречу
+ permission_view_meetings: Просмотреть встречи
+ permission_create_meeting_agendas: Управлять повестками дня
+ permission_close_meeting_agendas: Закрыть повестки дня
+ permission_send_meeting_agendas_notification: 'Отправить уведомление о реценизии повесток дня'
+ permission_create_meeting_minutes: Управление протоколами
+ permission_send_meeting_minutes_notification: Отправить уведомление о рецензии протокола
+ project_module_meetings: Совещания
+ text_in_hours: в часах
+ text_meeting_agenda_for_meeting: 'Повестка дня встречи «%{meeting}»'
+ text_meeting_agenda_open_are_you_sure: Несохраненное содержимое протокола будет потеряно! Продолжить?
+ text_meeting_minutes_for_meeting: 'Протокол совещания «%{meeting}»'
+ text_review_meeting_agenda: '%{author} поставил %{link} для рецензии.'
+ text_review_meeting_minutes: '%{author} поставил %{link} для рецензии.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/sk.yml b/vendored-plugins/openproject-meeting/config/locales/sk.yml
new file mode 100644
index 0000000000..cc77dc1547
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/sk.yml
@@ -0,0 +1,54 @@
+sk:
+ activerecord:
+ attributes:
+ meeting:
+ location: Miesto
+ duration: Trvanie
+ participants: Účastníci
+ participants_attended: Účastníci
+ participants_invited: Pozvaných
+ start_time: Čas
+ models:
+ meeting_agenda: Agenda
+ meeting_minutes: Zápisnica
+ description_attended: sa zúčastnil
+ description_invite: pozvaní
+ events:
+ meeting: Stretnutie upravené
+ meeting_agenda: Agenda stretnutia upravená
+ meeting_agenda_closed: Agenda stretnutia uzavretá
+ meeting_agenda_opened: Agenda stretnutia otvorená
+ meeting_minutes: Zápisnica zo stretnutia upravená
+ meeting_minutes_created: Zápisnica zo stretnutia vytvorená
+ error_notification_with_errors: 'Nepodarilo sa odoslať notifikáciu. Nasledovní príjemcovia nemohli byť oboznámení: %{recipients}'
+ label_meeting: Stretnutie
+ label_meeting_plural: Stretnutia
+ label_meeting_new: Nové stretnutie
+ label_meeting_edit: Upraviť stretnutie
+ label_meeting_agenda: Agenda
+ label_meeting_minutes: Zápisnica
+ label_meeting_close: Zatvoriť
+ label_meeting_open: Otvoriť
+ label_meeting_agenda_close: Uzavrieť agendu za účelom začatia logu stretnutia
+ label_meeting_date_time: Dátum/čas
+ label_meeting_diff: Rozdieľ
+ label_notify: Odoslať na revíziu
+ label_version: Verzia
+ notice_successful_notification: Notifikácia úspešne odoslaná
+ notice_timezone_missing: 'Časové pásmo nebolo špecificky nastavené, použilo sa teda %{zone}. Ak chcete vybrať iné časové pásmo, kliknite prosím tu.'
+ permission_create_meetings: Vytvárať stretnutia
+ permission_edit_meetings: Upravovať stretnutia
+ permission_delete_meetings: Odstraňovať stretnutia
+ permission_view_meetings: Prezerať stretnutia
+ permission_create_meeting_agendas: Spravovať stretnutia
+ permission_close_meeting_agendas: Definovať agendy stretnutí
+ permission_send_meeting_agendas_notification: Rozposielať notifikácie ne revízie agend stretnutí
+ permission_create_meeting_minutes: Spravovať zápisnice zo stretnutí
+ permission_send_meeting_minutes_notification: Rozposielať notifikácie na revízie zápisníc zo stretnutí
+ project_module_meetings: Stretnutia
+ text_in_hours: v hodinách
+ text_meeting_agenda_for_meeting: 'agenda stretnutia "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Neuložené obsah zápisnice stretnutia bude stratený! Chcete pokračovať?
+ text_meeting_minutes_for_meeting: 'zápisnica zo stretnutia "%{meeting}"'
+ text_review_meeting_agenda: '%{author} odoslal %{link} na revíziu.'
+ text_review_meeting_minutes: '%{author} odoslal %{link} na revíziu.'
diff --git a/vendored-plugins/openproject-meeting/config/locales/sv-SE.yml b/vendored-plugins/openproject-meeting/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..80d87d3e79
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/locales/sv-SE.yml
@@ -0,0 +1,54 @@
+sv:
+ activerecord:
+ attributes:
+ meeting:
+ location: Plats
+ duration: Varaktighet
+ participants: Deltagare
+ participants_attended: Deltagare
+ participants_invited: Inbjudna
+ start_time: Tid
+ models:
+ meeting_agenda: Agenda
+ meeting_minutes: Protokoll
+ description_attended: deltog
+ description_invite: inbjudna
+ events:
+ meeting: Mötet redigerat
+ meeting_agenda: Mötets agenda redigerat
+ meeting_agenda_closed: Mötets agenda stängt
+ meeting_agenda_opened: Mötets agenda öppnat
+ meeting_minutes: Mötesprotokoll redigerat
+ meeting_minutes_created: Mötesprotokoll skapat
+ error_notification_with_errors: 'Det gick inte att skicka underrättelse. Följande mottagare kunde inte underrättas: %{recipients}'
+ label_meeting: Möte
+ label_meeting_plural: Möten
+ label_meeting_new: Nytt möte
+ label_meeting_edit: Redigera möte
+ label_meeting_agenda: Agenda
+ label_meeting_minutes: Protokoll
+ label_meeting_close: Stäng
+ label_meeting_open: Öppna
+ label_meeting_agenda_close: Stäng agendan för att påbörja protokollet
+ label_meeting_date_time: Datum/tid
+ label_meeting_diff: Skillnad
+ label_notify: Skicka för granskning
+ label_version: Version
+ notice_successful_notification: Underrättelse skickades
+ notice_timezone_missing: 'Ingen tidszon angiven och %{zone} antas. För att välja din tidszon, vänligen klicka här.'
+ permission_create_meetings: Skapa möten
+ permission_edit_meetings: Redigera möten
+ permission_delete_meetings: Ta bort möten
+ permission_view_meetings: Visa möten
+ permission_create_meeting_agendas: Hantera agendor
+ permission_close_meeting_agendas: Stäng agendor
+ permission_send_meeting_agendas_notification: Skicka underrättelse om granskning av agendor
+ permission_create_meeting_minutes: Hantera protokoll
+ permission_send_meeting_minutes_notification: Skicka underrättelse om granskning av protokoll
+ project_module_meetings: Möten
+ text_in_hours: i timmar
+ text_meeting_agenda_for_meeting: 'agenda för mötet "%{meeting}"'
+ text_meeting_agenda_open_are_you_sure: Osparat innehåll i protokollet kommer att förloras! Vill du fortsätta?
+ text_meeting_minutes_for_meeting: 'protokollet för mötet "%{meeting}"'
+ text_review_meeting_agenda: '%{author} har lagt upp %{link} för granskning.'
+ text_review_meeting_minutes: '%{author} har lagt upp %{link} för granskning.'
diff --git a/vendored-plugins/openproject-meeting/config/routes.rb b/vendored-plugins/openproject-meeting/config/routes.rb
new file mode 100644
index 0000000000..3b8a8fed1f
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/config/routes.rb
@@ -0,0 +1,62 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+OpenProject::Application.routes.draw do
+
+ scope 'projects/:project_id' do
+ resources :meetings, only: [:new, :create, :index]
+ end
+
+ resources :meetings, except: [:new, :create, :index] do
+
+ resource :agenda, controller: 'meeting_agendas', only: [:update] do
+ member do
+ get :history
+ get :diff
+ put :close
+ put :open
+ put :notify
+ post :preview
+ end
+
+ resources :versions, only: [:show],
+ controller: 'meeting_agendas'
+ end
+
+ resource :minutes, controller: 'meeting_minutes', only: [:update] do
+ member do
+ get :history
+ get :diff
+ put :notify
+ post :preview
+ end
+
+ resources :versions, only: [:show],
+ controller: 'meeting_minutes'
+ end
+
+ member do
+ get :copy
+ match '/:tab' => 'meetings#show', :constraints => { tab: /(agenda|minutes)/ },
+ :via => :get,
+ :as => 'tab'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20111605171865_aggregated_meeting_migrations.rb b/vendored-plugins/openproject-meeting/db/migrate/20111605171865_aggregated_meeting_migrations.rb
new file mode 100644
index 0000000000..58c03b47b7
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20111605171865_aggregated_meeting_migrations.rb
@@ -0,0 +1,81 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'migration_squasher').to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedMeetingMigrations < ActiveRecord::Migration
+ MIGRATION_FILES = <<-MIGRATIONS
+ 20110106210555_create_meetings.rb
+ 20110106221214_create_meeting_contents.rb
+ 20110106221946_create_meeting_content_versions.rb
+ 20110108230721_create_meeting_participants.rb
+ 20110224180804_add_lock_to_meeting_content.rb
+ 20110819162852_create_initial_meeting_journals.rb
+ 20111605171815_merge_meeting_content_versions_with_journals.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = 'redmine_meeting'
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table 'meeting_contents' do |t|
+ t.string 'type'
+ t.integer 'meeting_id'
+ t.integer 'author_id'
+ t.text 'text'
+ t.integer 'lock_version'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ t.boolean 'locked', default: false
+ end
+
+ create_table 'meeting_participants' do |t|
+ t.integer 'user_id'
+ t.integer 'meeting_id'
+ t.integer 'meeting_role_id'
+ t.string 'email'
+ t.string 'name'
+ t.boolean 'invited'
+ t.boolean 'attended'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ end
+
+ create_table 'meetings' do |t|
+ t.string 'title'
+ t.integer 'author_id'
+ t.integer 'project_id'
+ t.string 'location'
+ t.datetime 'start_time'
+ t.float 'duration'
+ t.datetime 'created_at', null: false
+ t.datetime 'updated_at', null: false
+ end
+ end
+ end
+
+ def down
+ drop_table 'meeting_contents'
+ drop_table 'meeting_participants'
+ drop_table 'meetings'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20130731151542_remove_meeting_role_id_from_meeting_participants.rb b/vendored-plugins/openproject-meeting/db/migrate/20130731151542_remove_meeting_role_id_from_meeting_participants.rb
new file mode 100644
index 0000000000..45b25e6514
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20130731151542_remove_meeting_role_id_from_meeting_participants.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class RemoveMeetingRoleIdFromMeetingParticipants < ActiveRecord::Migration
+ def up
+ remove_column :meeting_participants, :meeting_role_id
+ end
+
+ def down
+ add_column :meeting_participants, :meeting_role_id, :integer
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20130822113942_create_meeting_journals.rb b/vendored-plugins/openproject-meeting/db/migrate/20130822113942_create_meeting_journals.rb
new file mode 100644
index 0000000000..88d7fad59b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20130822113942_create_meeting_journals.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+class CreateMeetingJournals < ActiveRecord::Migration
+ def change
+ create_table :meeting_journals do |t|
+ t.integer :journal_id, null: false
+ t.string :title
+ t.integer :author_id
+ t.integer :project_id
+ t.string :location
+ t.datetime :start_time
+ t.float :duration
+ end
+
+ create_table :meeting_content_journals do |t|
+ t.integer :journal_id, null: false
+ t.integer :meeting_id
+ t.integer :author_id
+ t.text :text
+ t.boolean :locked
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20130924091342_legacy_meeting_journal_data.rb b/vendored-plugins/openproject-meeting/db/migrate/20130924091342_legacy_meeting_journal_data.rb
new file mode 100644
index 0000000000..e7216b95e0
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20130924091342_legacy_meeting_journal_data.rb
@@ -0,0 +1,37 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+#
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'legacy_journal_migrator').to_s
+
+class LegacyMeetingJournalData < ActiveRecord::Migration
+ def up
+ migrator.run
+ end
+
+ def down
+ migrator.remove_journals_derived_from_legacy_journals 'meeting_journals'
+ end
+
+ def migrator
+ @migrator ||= Migration::LegacyJournalMigrator.new 'MeetingJournal', 'meeting_journals'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20130924093842_legacy_meeting_agenda_journal_data.rb b/vendored-plugins/openproject-meeting/db/migrate/20130924093842_legacy_meeting_agenda_journal_data.rb
new file mode 100644
index 0000000000..08d0518a42
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20130924093842_legacy_meeting_agenda_journal_data.rb
@@ -0,0 +1,62 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+#
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'legacy_journal_migrator').to_s
+
+class LegacyMeetingAgendaJournalData < ActiveRecord::Migration
+ class UnsupportedMeetingAgendaJournalCompressionError < ::StandardError
+ end
+
+ def up
+ migrator.run
+ end
+
+ def down
+ migrator.remove_journals_derived_from_legacy_journals 'meeting_content_journals'
+ end
+
+ def migrator
+ @migrator ||= Migration::LegacyJournalMigrator.new 'MeetingAgendaJournal', 'meeting_content_journals' do
+
+ self.journable_class = 'MeetingContent'
+
+ def migrate_key_value_pairs!(to_insert, _legacy_journal, _journal_id)
+ if to_insert.has_key?('data')
+
+ # Why is that checked but than the compression is not used in any way to read the data
+ if !to_insert.has_key?('compression')
+
+ raise UnsupportedMeetingAgendaJournalCompressionError, <<-MESSAGE.split("\n").map(&:strip!).join(' ') + "\n"
+ There is a MeetingAgenda journal that contains data in an
+ unsupported compression: #{compression}
+ MESSAGE
+
+ end
+
+ # as the old journals used the format [old_value, new_value] we have to fake it here
+ to_insert['text'] = [nil, to_insert.delete('data')]
+ end
+ end
+
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20130924114042_legacy_meeting_minutes_journal_data.rb b/vendored-plugins/openproject-meeting/db/migrate/20130924114042_legacy_meeting_minutes_journal_data.rb
new file mode 100644
index 0000000000..fbf34548a0
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20130924114042_legacy_meeting_minutes_journal_data.rb
@@ -0,0 +1,62 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+#
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'legacy_journal_migrator').to_s
+
+class LegacyMeetingMinutesJournalData < ActiveRecord::Migration
+ class UnsupportedMeetingMinutesJournalCompressionError < ::StandardError
+ end
+
+ def up
+ migrator.run
+ end
+
+ def down
+ migrator.remove_journals_derived_from_legacy_journals 'meeting_content_journals'
+ end
+
+ def migrator
+ @migrator ||= Migration::LegacyJournalMigrator.new 'MeetingMinutesJournal', 'meeting_content_journals' do
+
+ self.journable_class = 'MeetingContent'
+
+ def migrate_key_value_pairs!(to_insert, _legacy_journal, _journal_id)
+ if to_insert.has_key?('data')
+
+ # Why is that checked but than the compression is not used in any way to read the data
+ if !to_insert.has_key?('compression')
+
+ raise UnsupportedMeetingMinutesJournalCompressionError, <<-MESSAGE.split("\n").map(&:strip!).join(' ') + "\n"
+ There is a MeetingMinutes journal that contains data in an
+ unsupported compression: #{compression}
+ MESSAGE
+
+ end
+
+ # as the old journals used the format [old_value, new_value] we have to fake it here
+ to_insert['text'] = [nil, to_insert.delete('data')]
+ end
+ end
+
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/db/migrate/20131127120534_migrate_text_references_to_work_packages.rb b/vendored-plugins/openproject-meeting/db/migrate/20131127120534_migrate_text_references_to_work_packages.rb
new file mode 100644
index 0000000000..b0a0dac54b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/db/migrate/20131127120534_migrate_text_references_to_work_packages.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require Rails.root.join('db', 'migrate', 'migration_utils', 'text_references').to_s
+
+class MigrateTextReferencesToWorkPackages < ActiveRecord::Migration
+ include Migration::Utils
+
+ COLUMNS_PER_TABLE = {
+ 'meeting_contents' => { columns: ['text'], update_journal: true },
+ }
+
+ def up
+ COLUMNS_PER_TABLE.each_pair do |table, options|
+ say_with_time_silently "Update text references for table #{table}" do
+ update_text_references(table, options[:columns], options[:update_journal])
+ end
+ end
+ end
+
+ def down
+ COLUMNS_PER_TABLE.each_pair do |table, options|
+ say_with_time_silently "Restore text references for table #{table}" do
+ restore_text_references(table, options[:columns], options[:update_journal])
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/doc/COPYRIGHT.md b/vendored-plugins/openproject-meeting/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..37204bec74
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject Meeting Plugin
+
+This plugin adds functions to support project meetings to
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-meeting/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-meeting/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..088671197b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/doc/COPYRIGHT_short.md
@@ -0,0 +1,17 @@
+OpenProject Meeting Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
diff --git a/vendored-plugins/openproject-meeting/doc/GPL.txt b/vendored-plugins/openproject-meeting/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-meeting/features/meeting_close.feature b/vendored-plugins/openproject-meeting/features/meeting_close.feature
new file mode 100644
index 0000000000..d7169d101f
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meeting_close.feature
@@ -0,0 +1,103 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Close and open meeting agendas
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is 1 user with:
+ | login | bob |
+ And there is a role "user"
+ And the user "alice" is a "user" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+
+ @javascript
+ Scenario: Navigate to a meeting page with no permission to close meeting agendas
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ When I am already logged in as "alice"
+ And I go to the show page of the meeting called "Bobs Meeting"
+ And I follow "Agenda"
+ Then I should not see "Close" within ".meeting_agenda"
+
+ @javascript
+ Scenario: Navigate to a meeting page with permission to close the meeting agenda and go to the minutes
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | close_meeting_agendas |
+ When I am already logged in as "alice"
+ And I go to the show page of the meeting called "Bobs Meeting"
+ And I follow "Minutes" within ".tabs"
+ Then I should not see "Edit" within ".meeting_minutes"
+ And I should see "Close the agenda to begin the Minutes" within ".meeting_minutes"
+
+ @javascript
+ Scenario: Navigate to a meeting page with permission to close and close the meeting agenda copies the text and shows the meeting
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | close_meeting_agendas |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | text | "blubber" |
+ When I am already logged in as "alice"
+ And I go to the show page of the meeting called "Bobs Meeting"
+ And I follow "Close" within ".meeting_agenda"
+
+ Then I should be on the show page of the meeting called "Bobs Meeting"
+ And the minutes should contain the following text:
+ | blubber |
+
+ When I follow "Agenda"
+ Then I should not see "Close" within ".meeting_agenda"
+ And I should see "Open" within ".meeting_agenda"
+
+ @javascript
+ Scenario: Navigate to a meeting page with permission to close and open the meeting agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | close_meeting_agendas |
+ # This won't work because the needed "click on open" has a confirm() which cucumber doesn't seem to handle
+ # And the meeting "Bobs Meeting" has 1 agenda with:
+ # | locked | true |
+ When I am already logged in as "alice"
+ And I go to the show page of the meeting called "Bobs Meeting"
+ And I follow "Agenda"
+ # And I click on "Open"
+ Then I should not see "Open" within ".meeting_agenda"
+ And I should see "Close" within ".meeting_agenda"
+
+ @javascript
+ Scenario: Navigate to a meeting page with a closed meeting agenda and permission to edit meeting agendas
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_agendas |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+ When I am already logged in as "alice"
+ And I go to the show page of the meeting called "Bobs Meeting"
+ And I follow "Agenda"
+ Then I should not see "Edit" within ".meeting_agenda"
diff --git a/vendored-plugins/openproject-meeting/features/meeting_participants.feature b/vendored-plugins/openproject-meeting/features/meeting_participants.feature
new file mode 100644
index 0000000000..b15505cfca
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meeting_participants.feature
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Meeting has participants
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | firstname | Alice |
+ | lastname | Alice |
+ | language | en |
+ And there is 1 user with:
+ | login | bob |
+ | firstname | Bob |
+ | lastname | Bobbit |
+ | language | en |
+ And there is a role "meeting_viewer"
+ And there is a role "meeting_editor"
+ And the role "meeting_viewer" may have the following rights:
+ | view_meetings |
+ And the role "meeting_editor" may have the following rights:
+ | view_meetings |
+ | edit_meetings |
+ And the user "bob" is a "meeting_editor" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ Given I am already logged in as "bob"
+
+ Scenario: Users not allowed to view meetings are no valid participants
+ When I go to the edit page of the meeting called "Bobs Meeting"
+ Then the user "alice" should not be available as a participant
+
+ Scenario: Users allowed to view meetings are valid participants
+ Given the user "alice" is a "meeting_viewer" in the project "dingens"
+ When I go to the edit page of the meeting called "Bobs Meeting"
+ Then the user "alice" should be available as a participant
diff --git a/vendored-plugins/openproject-meeting/features/meeting_update.feature b/vendored-plugins/openproject-meeting/features/meeting_update.feature
new file mode 100644
index 0000000000..6d625aca0f
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meeting_update.feature
@@ -0,0 +1,68 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Updating meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | name | worlddomination |
+ And the project "worlddomination" uses the following modules:
+ | meetings |
+ And there is a role "user"
+ And the role "user" may have the following rights:
+ | view_meetings |
+ | edit_meetings |
+ And there is 1 user with:
+ | login | alice |
+ | firstname | alice |
+ | lastname | alice |
+ And there is 1 user with:
+ | login | bob |
+ | firstname | bob |
+ | lastname | bobbit |
+ And there is 1 user with:
+ | login | chuck |
+ | firstname | chuck |
+ | lastname | testa |
+ And the user "alice" is a "user" in the project "worlddomination"
+ And the user "bob" is a "user" in the project "worlddomination"
+ And the user "chuck" is a "user" in the project "worlddomination"
+ And there is 1 meeting in project "worlddomination" created by "alice" with:
+ | title | Meeting 1 |
+ | location | Room 1 |
+ | duration | 1:30 |
+ | start_time | 2011-02-11 12:30:00 |
+ And "bob" is invited to the Meeting "Meeting 1"
+
+ Scenario: Adding a new invitee
+ When I am already logged in as "alice"
+ And I go to the edit page of the meeting called "Meeting 1"
+ And I check "chuck testa invited"
+ And I click on "Save"
+ Then I should see "Successful update."
+ And I should see "chuck testa"
+
+ Scenario: Removing an invitee
+ When I am already logged in as "alice"
+ And I go to the edit page of the meeting called "Meeting 1"
+ And I uncheck "bob bobbit invited"
+ And I click on "Save"
+ Then I should see "Successful update."
+ And I should not see "bob bobbit"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_activity.feature b/vendored-plugins/openproject-meeting/features/meetings_activity.feature
new file mode 100644
index 0000000000..3ffdb96c02
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_activity.feature
@@ -0,0 +1,71 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Show meeting activity
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ | activity |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ | admin | true |
+ And there is a role "user"
+ And the role "user" may have the following rights:
+ | view_meetings |
+ | edit_meetings |
+ And the user "alice" is a "user" in the project "dingens"
+ And the user "alice" has the following preferences
+ | time_zone | UTC |
+ And there is 1 user with:
+ | login | bob |
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ | location | Room 2 |
+ | duration | 2.5 |
+ | start_time | 2011-02-10 11:00:00 |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+ | text | foobaz |
+ And the meeting "Bobs Meeting" has minutes with:
+ | text | barbaz |
+ And I am already logged in as "alice"
+
+ Scenario: Navigate to the project's activity page and see the meeting activity
+ When I go to the meetings activity page for the project "dingens"
+ And I activate activity filter "Meetings"
+ When I click "Apply"
+ Then I should see "Meeting: Bobs Meeting (02/10/2011 11:00 AM-01:30 PM)" within "dt.meeting > a"
+ And I should see "Agenda: Bobs Meeting" within ".meeting-agenda"
+ And I should see "Minutes: Bobs Meeting" within ".meeting-minutes"
+
+ Scenario: Change a metadata on a meeting and see the activity on the project's activity page
+ When I go to the edit page for the meeting called "Bobs Meeting"
+ And I fill in the following:
+ | meeting_location | Geheimer Ort! |
+ And I press "Save"
+ And I go to the meetings activity page for the project "dingens"
+ And I activate activity filter "Meetings"
+ When I click "Apply"
+ Then I should see "Meeting: Bobs Meeting (02/10/2011 11:00 AM-01:30 PM)" within ".meeting.me"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_copy.feature b/vendored-plugins/openproject-meeting/features/meetings_copy.feature
new file mode 100644
index 0000000000..6ffd1c1f89
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_copy.feature
@@ -0,0 +1,112 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Copy meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ | firstname | Alice |
+ | lastname | Alice |
+ And the user "alice" has the following preferences
+ | time_zone | UTC |
+ And there is 1 user with:
+ | login | bob |
+ And there is 1 user with:
+ | login | charly |
+ And there is 1 user with:
+ | login | dave |
+ And there is a role "user"
+ And the user "alice" is a "user" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "alice" with:
+ | title | Alices Meeting |
+ | location | CZI |
+ | duration | 1.5 |
+ | start_time | 2013-03-27 18:55:00 |
+
+ Scenario: Navigate to a meeting page with permission to create meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ Then I should see "Copy" within ".meeting--main-toolbar"
+
+ Scenario: Navigate to a meeting copy page
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ And I click on "Copy"
+ Then the "meeting[title]" field should contain "Alices Meeting"
+ And the "meeting[location]" field should contain "CZI"
+ And the "meeting[duration]" field should contain "1.5"
+ And the "meeting[start_date]" field should contain "2013-03-27"
+ And the "meeting[start_time_hour]" field should contain "18:55"
+ #And no participant should be selected as attendee
+ #And only invited participants should be selected as invitees
+
+ Scenario: Navigate to a meeting copy page to make sure the author is selected as invited but not as attendee
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ And "alice" attended the Meeting "Alices Meeting"
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ And I click on "Copy"
+ Then the "meeting[participants_attributes][][invited]" checkbox should be checked
+ And the "meeting[participants_attributes][][attended]" checkbox should not be checked
+
+ Scenario: Copy a meeting and make sure the author isn''t copied over
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ And I click on "Copy"
+ And I click on "Create"
+ Then I should not see "Alice Alice; Alice Alice"
+ And I should see "Alice Alice"
+
+ Scenario: Copy a meeting and make sure the agenda is copied over
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ And the meeting "Alices Meeting" has 1 agenda with:
+ | text | "blubber" |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ And I follow "Copy"
+ And I press "Create"
+ And I follow "Agenda"
+ And I follow "History" within ".meeting_agenda"
+ Then I should see "Copied from Meeting #"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_delete.feature b/vendored-plugins/openproject-meeting/features/meetings_delete.feature
new file mode 100644
index 0000000000..f62d270464
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_delete.feature
@@ -0,0 +1,84 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Delete meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is 1 user with:
+ | login | bob |
+ And there is a role "user"
+ And the user "alice" is a "user" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "alice" with:
+ | title | Alices Meeting |
+ | location | Room 1 |
+ | duration | 1:30 |
+ | start_time | 2011-02-11 12:30:00 |
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ | location | Room 2 |
+ | duration | 2:30 |
+ | start_time | 2011-02-10 11:00:00 |
+
+ Scenario: Navigate to an other-created meeting with no permission to delete meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Bobs Meeting"
+ Then I should not see "Delete"
+
+ Scenario: Navigate to a self-created meeting with permission to delete meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | delete_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Alices Meeting"
+ Then I should see "Delete"
+
+ Scenario: Navigate to an other-created meeting with permission to delete meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | delete_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Bobs Meeting"
+ Then I should see "Delete"
+
+ @javascript
+ Scenario: Delete a meeting with permission to delete meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | delete_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Bobs Meeting"
+ And I click on "Delete"
+ And I confirm the JS confirm dialog
+ Then I should see "Meetings"
+ But I should not see "Bobs Meeting"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_index.feature b/vendored-plugins/openproject-meeting/features/meetings_index.feature
new file mode 100644
index 0000000000..439b0eea5f
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_index.feature
@@ -0,0 +1,97 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Show existing meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is a role "user"
+ And the role "user" may have the following rights:
+ | view_meetings |
+ And the user "alice" is a "user" in the project "dingens"
+
+ Scenario: Navigate to the meeting index page with no meetings
+ When I am already logged in as "alice"
+ And I go to the page for the project "dingens"
+ And I click on "Meetings"
+ Then I should see "Meetings" within "#content"
+ And I should see "No data to display" within "#content"
+
+ Scenario: Navigate to the meeting index page with 2 meetings
+ Given there is 1 meeting in project "dingens" created by "alice" with:
+ | title | Meeting 1 |
+ | location | Room 1 |
+ | duration | 1:30 |
+ | start_time | 2011-02-11 12:30:00 |
+ And there is 1 meeting in project "dingens" created by "alice" with:
+ | title | Meeting 2 |
+ | location | Room 2 |
+ | duration | 2:30 |
+ | start_time | 2011-02-10 11:00:00 |
+ When I am already logged in as "alice"
+ And I go to the page for the project "dingens"
+ And I click on "Meetings"
+ Then I should see "Meetings" within "#content"
+ But I should not see "No data to display" within "#content"
+ And I should see 4 meetings
+
+ Scenario: Lots of Meetings are split into pages
+ Given we paginate after 3 items
+ Given there is 3 meetings in project "dingens" that start 0 days from now with:
+ | title | Meeting Today |
+ Given there is 2 meetings in project "dingens" that start -1 days from now with:
+ | title | Meeting Last Week |
+ When I am already logged in as "alice"
+ And I go to the page for the project "dingens"
+ And I click on "Meetings"
+ # see above: means 3 meetings
+ Then I should see 6 meetings
+ And I should see "Meeting Today"
+ But I should not see "Meeting Last Week"
+ When I click on "2"
+ # means 2 meetings
+ Then I should see 4 meetings
+ And I should not see "Meeting Today"
+ But I should see "Meeting Last Week"
+
+ Scenario: Jumps to page of current date when no page given
+ Given we paginate after 3 items
+ Given there is 5 meetings in project "dingens" that start +7 days from now with:
+ | title | Meeting Next Week |
+ Given there is 5 meetings in project "dingens" that start 1 days from now with:
+ | title | Meeting Tomorrow |
+ Given there is 5 meetings in project "dingens" that start 0 days from now with:
+ | title | Meeting Today |
+ Given there is 5 meetings in project "dingens" that start -7 days from now with:
+ | title | Meeting Last Week |
+ When I am already logged in as "alice"
+ And I go to the page for the project "dingens"
+ And I click on "Meetings"
+ Then I should see "Meeting Today"
+ And I should see "Meeting Tomorrow"
+ But I should not see "Meeting Last Week"
+ And I should not see "Meeting Next Week"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_locking.feature b/vendored-plugins/openproject-meeting/features/meetings_locking.feature
new file mode 100644
index 0000000000..50e6869590
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_locking.feature
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Locking meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | bob |
+ And there is a role "user"
+ And the user "bob" is a "user" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | text | awesome! |
+
+ @javascript
+ Scenario: Save a meeting after it has changed while editing
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ | create_meeting_agendas |
+ | edit_meetings |
+ When I am already logged in as "bob"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "Bobs Meeting"
+ And I follow "Edit" within ".meeting_agenda"
+ # Change the text of the agenda to create an editing conflict
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | text | and now for something completely different |
+ And I fill in "Blabla oder?" for "meeting_agenda_text"
+ And I click on "Save"
+ Then I should see "Information has been updated by at least one other user in the meantime."
+ # Prüfen, ob die Editbox noch sichtbar ist
+ #And I should see "Text formatting" within "#tab-content-agenda"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_new.feature b/vendored-plugins/openproject-meeting/features/meetings_new.feature
new file mode 100644
index 0000000000..4a99843f38
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_new.feature
@@ -0,0 +1,107 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Create new meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is a role "user"
+ And the user "alice" is a "user" in the project "dingens"
+
+ Scenario: Navigate to the meeting index page with no permission to create new meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ Then I should not see "New Meeting"
+
+ Scenario: Navigate to the meeting index page with permission to create new meetings
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ Then I should see "New Meeting"
+
+ Scenario: Create a new meeting with no title
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "New Meeting"
+ And I click on "Create"
+ Then I should see "Title can't be blank"
+
+ Scenario Outline: Create a new meeting with a title and a date, time, and duration with no and different time zones set
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And the user "alice" has the following preferences
+ | time_zone | |
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "New Meeting"
+ And I fill in the following:
+ | meeting_title | FSR Sitzung 123 |
+ | meeting_start_date | 2013-03-28 |
+ | meeting_start_time_hour | 13:30 |
+ | meeting_duration | 1.5 |
+ And I click on "Create"
+ Then I should see "Successful creation."
+ And I should see "FSR Sitzung 123"
+ And I should see "03/28/2013 01:30 PM - 03:00 PM"
+
+ Examples:
+ | t_zone |
+ | CET |
+ | UTC |
+ | |
+ | Pacific Time (US & Canada) |
+
+ Scenario: Visit the new meeting page to make sure the author is selected as invited
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meetings |
+ When I am already logged in as "alice"
+ And I go to the Meetings page for the project called "dingens"
+ And I click on "New Meeting"
+ Then the "meeting[participants_attributes][][invited]" checkbox should be checked
+
+ Scenario: Create a meeting in a project without members shouldn't error out
+ Given there is 1 project with the following:
+ | identifier | foreverempty |
+ | name | foreverempty |
+ And the project "foreverempty" uses the following modules:
+ | meetings |
+ When I am already admin
+ And I go to the Meetings page for the project called "foreverempty"
+ And I click on "New Meeting"
+ And I fill in the following:
+ | meeting_title | Emtpy Meetings |
+ And I press "Create"
+ Then I should see "Successful creation."
diff --git a/vendored-plugins/openproject-meeting/features/meetings_search.feature b/vendored-plugins/openproject-meeting/features/meetings_search.feature
new file mode 100644
index 0000000000..e3b7ca94ac
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_search.feature
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Search meetings through the global search
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is a role "user"
+ And the role "user" may have the following rights:
+ | view_meetings |
+ And the user "alice" is a "user" in the project "dingens"
+ And there is 1 user with:
+ | login | bob |
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ | location | Room 2 |
+ | duration | 2:30 |
+ | start_time | 2011-02-10 11:00:00 |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+ | text | foobaz |
+ And the meeting "Bobs Meeting" has minutes with:
+ | text | barbaz |
+
+ @javascript
+ Scenario: Navigate to the search page and search for a meeting
+ When I am already logged in as "alice"
+ And I go to the search page
+ And I fill in the following:
+ | search-input | bob |
+ And I click on "Submit"
+ Then I should see "Bobs Meeting" within "#search-results .meeting"
diff --git a/vendored-plugins/openproject-meeting/features/meetings_show.feature b/vendored-plugins/openproject-meeting/features/meetings_show.feature
new file mode 100644
index 0000000000..a51d01dbf0
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/meetings_show.feature
@@ -0,0 +1,145 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Show meetings
+
+ Background:
+ Given there is 1 project with the following:
+ | identifier | dingens |
+ | name | dingens |
+ And the project "dingens" uses the following modules:
+ | meetings |
+ And there is 1 user with:
+ | login | alice |
+ | language | en |
+ And there is 1 user with:
+ | login | bob |
+ And there is a role "user"
+ And the user "alice" is a "user" in the project "dingens"
+ And there is 1 meeting in project "dingens" created by "bob" with:
+ | title | Bobs Meeting |
+ Given I am already logged in as "alice"
+
+ Scenario: Navigate to a meeting page
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ When I go to the Meetings page for the project called "dingens"
+ And I follow "Bobs Meeting"
+ Then I should be on the show page for the meeting called "Bobs Meeting"
+
+ Scenario: Navigate to a meeting page with an open agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+
+ Then I should see "Agenda" within ".meeting_agenda"
+ And I should see "No data to display" within ".meeting_agenda"
+
+ Scenario: Navigate to a meeting page with a closed agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+
+ Then I should see "Minutes" within ".meeting_minutes"
+ And I should see "No data to display" within ".meeting_minutes"
+
+ @javascript
+ Scenario: Navigate to a meeting page with an open agenda and the permission to edit the agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_agendas |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+
+ Then I should see "Agenda" within ".meeting_agenda"
+ And I should not see "No data to display" within "#meeting_agenda_text"
+ And there should be a text edit toolbar for the "#meeting_agenda_text" field
+
+ @javascript
+ Scenario: Navigate to a meeting page with a closed agenda and the permission to edit the minutes
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_minutes |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+
+ When I go to the show page of the meeting called "Bobs Meeting"
+
+ Then I should see "Minutes" within ".meeting_minutes"
+ And I should not see "No data to display" within "#meeting_minutes_text"
+ And there should be a text edit toolbar for the "#meeting_minutes_text" field
+
+ @javascript
+ Scenario: Navigate to a meeting page with an open agenda and the permission to edit the minutes
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_minutes |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+ # Make sure we're on the right tab
+ And I click on "Minutes"
+
+ Then I should not see "Edit" within ".meeting_minutes"
+
+ @javascript
+ Scenario: Navigate to a meeting page with a closed agenda and the permission to edit the agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_agendas |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+ # Make sure we're on the right tab
+ And I click on "Agenda"
+
+ Then I should not see "Edit" within ".meeting_agenda"
+
+ Scenario: Navigate to a meeting page with a closed agenda and the permission to edit the minutes and save minutes
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ | create_meeting_minutes |
+ And the meeting "Bobs Meeting" has 1 agenda with:
+ | locked | true |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+ And I fill in "meeting_minutes[text]" with "Some minutes!"
+ And I click on "Save"
+
+ Then I should see "Minutes" within ".meeting_minutes"
+ And I should see "Some minutes!" within "#meeting_minutes_text"
+
+ Scenario: Navigate to a meeting page and view an older version of an agenda
+ Given the role "user" may have the following rights:
+ | view_meetings |
+ And the Meeting "Bobs Meeting" has 1 agenda with:
+ | text | blah |
+ And the Meeting "Bobs Meeting" has 1 agenda with:
+ | text | foo |
+
+ When I go to the show page for the meeting called "Bobs Meeting"
+ And I follow "History" within ".meeting_agenda"
+ And I follow "1" within "table.list"
+ Then I should see "Agenda" within ".meeting_agenda"
+ And I should see "blah" within ".meeting_agenda"
diff --git a/vendored-plugins/openproject-meeting/features/step_definitions/meeting_steps.rb b/vendored-plugins/openproject-meeting/features/step_definitions/meeting_steps.rb
new file mode 100644
index 0000000000..5765c300bd
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/step_definitions/meeting_steps.rb
@@ -0,0 +1,85 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Given /^there is (\d+) [Mm]eetings? in project "(.+)" created by "(.+)" with:$/ do |count, project, user, table|
+ count.to_i.times do
+ m = FactoryGirl.build(:meeting)
+ m.project = Project.find_by_name(project)
+ m.author = User.find_by_login(user)
+ send_table_to_object(m, table)
+ end
+end
+
+Given /^there is (\d+) [Mm]eetings? in project "(.+)" that start (.*) days? from now with:$/ do |count, project, time, table|
+ count.to_i.times do
+ m = FactoryGirl.build(:meeting, start_time: Time.now + time.to_i.days)
+ m.project = Project.find_by_name(project)
+ send_table_to_object(m, table)
+ end
+end
+
+Given /^the [Mm]eeting "(.+)" has 1 agenda with:$/ do |meeting, table|
+ m = Meeting.find_by_title(meeting)
+ ma = MeetingAgenda.find_by_meeting_id(m.id) || FactoryGirl.build(:meeting_agenda, meeting: m)
+ send_table_to_object(ma, table)
+end
+
+Given /^the [Mm]eeting "(.+)" has 1 agenda$/ do |meeting|
+ m = Meeting.find_by_title(meeting)
+ m.agenda ||= FactoryGirl.build(:meeting_agenda)
+ m.save!
+end
+
+Given /^the [Mm]eeting "(.+)" has minutes with:$/ do |meeting, table|
+ m = Meeting.find_by_title(meeting)
+ mm = MeetingMinutes.find_by_meeting_id(m.id) || FactoryGirl.build(:meeting_minutes, meeting: m)
+ send_table_to_object(mm, table)
+end
+
+Given /^"(.+)" is invited to the [Mm]eeting "(.+)"$/ do |user, meeting|
+ m = Meeting.find_by_title(meeting)
+ p = m.participants.detect { |p| p.user_id = User.find_by_login(user).id } || FactoryGirl.build(:meeting_participant, meeting: m)
+ p.invited = true
+ p.save
+end
+
+Given /^"(.+)" attended the [Mm]eeting "(.+)"$/ do |user, meeting|
+ m = Meeting.find_by_title(meeting)
+ p = m.participants.detect { |p| p.user_id = User.find_by_login(user).id } || FactoryGirl.build(:meeting_participant, meeting: m)
+ p.attended = true
+ p.save
+end
+
+When /the agenda of the meeting "(.+)" changes meanwhile/ do |meeting|
+ m = Meeting.find_by_title(meeting)
+ m.agenda.text = 'oder oder?'
+ m.agenda.save!
+end
+
+Then /^the minutes should contain the following text:$/ do |table|
+ step %{I should see "#{table.raw.first.first}" within "#meeting_minutes-text"}
+end
+
+Then /^there should be a text edit toolbar for the "(.+)" field$/ do |field_id|
+ # second parent up
+ ancestor = find(:xpath, "//*[@id='#{field_id.gsub('#','')}']/../..")
+
+ expect(ancestor).to have_selector('.jstElements')
+end
diff --git a/vendored-plugins/openproject-meeting/features/support/participant_steps.rb b/vendored-plugins/openproject-meeting/features/support/participant_steps.rb
new file mode 100644
index 0000000000..3501d986eb
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/support/participant_steps.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Then(/^the user "(.*?)" should( not)? be available as a participant$/) do |login, negative|
+ user = User.find_by_login(login)
+
+ step(%{I should#{negative} see "#{user.name}" within "#meeting-form table.form--matrix"})
+end
diff --git a/vendored-plugins/openproject-meeting/features/support/paths.rb b/vendored-plugins/openproject-meeting/features/support/paths.rb
new file mode 100644
index 0000000000..7a8757a839
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/features/support/paths.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module MeetingNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the (\w+?) activity page for the [pP]roject "(.+?)"$/
+ project = get_project($2)
+ "/projects/#{project.identifier}/activity?show_#{$1}=1"
+
+ when /^the show page (?:of|for) the meeting called "(.+?)"$/
+ meeting = Meeting.find_by_title($1)
+
+ "/meetings/#{meeting.id}"
+ when /^the edit page (?:of|for) the meeting called "(.+?)"$/
+ meeting = Meeting.find_by_title($1)
+
+ "/meetings/#{meeting.id}/edit"
+ else
+ super
+ end
+ end
+end
+
+World(MeetingNavigationHelpers)
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting.rb
new file mode 100644
index 0000000000..70c68cc667
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module Meeting
+ require 'open_project/meeting/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/default_data.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/default_data.rb
new file mode 100644
index 0000000000..7845a592f9
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/default_data.rb
@@ -0,0 +1,42 @@
+module OpenProject
+ module Meeting
+ module DefaultData
+ module_function
+
+ def load!
+ add_permissions! (member_role || raise('Member role not found')), member_permissions
+ add_permissions! (reader_role || raise('Reader role not found')), reader_permissions
+ end
+
+ def add_permissions!(role, permissions)
+ role.add_permission! *permissions
+ end
+
+ def member_role
+ Role.find_by name: I18n.t(:default_role_member)
+ end
+
+ def member_permissions
+ [
+ :create_meetings,
+ :edit_meetings,
+ :delete_meetings,
+ :view_meetings,
+ :create_meeting_agendas,
+ :close_meeting_agendas,
+ :send_meeting_agendas_notification,
+ :create_meeting_minutes,
+ :send_meeting_minutes_notification
+ ]
+ end
+
+ def reader_role
+ Role.find_by name: I18n.t(:default_role_reader)
+ end
+
+ def reader_permissions
+ [:view_meetings]
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/engine.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/engine.rb
new file mode 100644
index 0000000000..bd6701e62b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/engine.rb
@@ -0,0 +1,84 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/plugins'
+
+module OpenProject::Meeting
+ class Engine < ::Rails::Engine
+ engine_name :openproject_meeting
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-meeting',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0' do
+
+ project_module :meetings do
+ permission :create_meetings, { meetings: [:new, :create, :copy] }, require: :member
+ permission :edit_meetings, { meetings: [:edit, :update] }, require: :member
+ permission :delete_meetings, { meetings: [:destroy] }, require: :member
+ permission :view_meetings, meetings: [:index, :show], meeting_agendas: [:history, :show, :diff], meeting_minutes: [:history, :show, :diff]
+ permission :create_meeting_agendas, { meeting_agendas: [:update, :preview] }, require: :member
+ permission :close_meeting_agendas, { meeting_agendas: [:close, :open] }, require: :member
+ permission :send_meeting_agendas_notification, { meeting_agendas: [:notify] }, require: :member
+ permission :create_meeting_minutes, { meeting_minutes: [:update, :preview] }, require: :member
+ permission :send_meeting_minutes_notification, { meeting_minutes: [:notify] }, require: :member
+ end
+
+ Redmine::Search.map do |search|
+ search.register :meetings
+ end
+
+ menu :project_menu, :meetings, { controller: '/meetings', action: 'index' },
+ caption: :project_module_meetings,
+ param: :project_id,
+ after: :wiki,
+ html: { class: 'icon2 icon-meetings' }
+
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable 'meeting_minutes'
+ end
+
+ Redmine::Activity.map do |activity|
+ activity.register :meetings, class_name: 'Activity::MeetingActivityProvider', default: false
+ end
+ end
+
+ patches [:Project]
+ patch_with_namespace :BasicData, :RoleSeeder
+ patch_with_namespace :BasicData, :SettingSeeder
+
+ initializer 'meeting.precompile_assets' do
+ Rails.application.config.assets.precompile += %w(meeting/meeting.css)
+ end
+
+ initializer 'meeting.register_hooks' do
+ require 'open_project/meeting/hooks'
+ end
+
+ config.to_prepare do
+ # load classes so that all User.before_destroy filters are loaded
+ require_dependency 'meeting'
+ require_dependency 'meeting_agenda'
+ require_dependency 'meeting_minutes'
+ require_dependency 'meeting_participant'
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/hooks.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/hooks.rb
new file mode 100644
index 0000000000..d3da64b016
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/hooks.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::Meeting
+ class Hooks < Redmine::Hook::ViewListener
+ render_on :activity_index_head,
+ partial: 'hooks/meetings/activity_index_head'
+
+ render_on :users_show_head,
+ partial: 'hooks/meetings/activity_index_head'
+
+ render_on :search_index_head,
+ partial: 'hooks/meetings/activity_index_head'
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/project_patch.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/project_patch.rb
new file mode 100644
index 0000000000..3c6838036a
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/project_patch.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::Meeting
+ module Patches
+ module ProjectPatch
+ def self.included(receiver)
+ receiver.class_eval do
+ has_many :meetings, -> { includes(:author) }, dependent: :destroy
+ end
+ end
+ end
+ end
+end
+
+Project.send(:include, OpenProject::Meeting::Patches::ProjectPatch)
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/role_seeder_patch.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/role_seeder_patch.rb
new file mode 100644
index 0000000000..f67fae6fe3
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/role_seeder_patch.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Meeting::Patches::RoleSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def seed_data!
+ super.tap do |_|
+ OpenProject::Meeting::DefaultData.load!
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/setting_seeder_patch.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/setting_seeder_patch.rb
new file mode 100644
index 0000000000..3d63875c07
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/patches/setting_seeder_patch.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::Meeting::Patches::SettingSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def data
+ original_data = super
+
+ unless original_data['default_projects_modules'].include? 'meetings'
+ original_data['default_projects_modules'] << 'meetings'
+ end
+
+ original_data
+ end
+ end
+end
+
diff --git a/vendored-plugins/openproject-meeting/lib/open_project/meeting/version.rb b/vendored-plugins/openproject-meeting/lib/open_project/meeting/version.rb
new file mode 100644
index 0000000000..5420f525a4
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/open_project/meeting/version.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module Meeting
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/lib/openproject-meeting.rb b/vendored-plugins/openproject-meeting/lib/openproject-meeting.rb
new file mode 100644
index 0000000000..b11b325f9e
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/lib/openproject-meeting.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/meeting'
diff --git a/vendored-plugins/openproject-meeting/openproject-meeting.gemspec b/vendored-plugins/openproject-meeting/openproject-meeting.gemspec
new file mode 100644
index 0000000000..807849f5c0
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/openproject-meeting.gemspec
@@ -0,0 +1,28 @@
+# encoding: UTF-8
+$:.push File.expand_path('../lib', __FILE__)
+
+# Maintain your gem's version:
+require 'open_project/meeting/version'
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = 'openproject-meeting'
+ s.version = OpenProject::Meeting::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/plugin-meetings'
+ s.summary = 'OpenProject Meeting'
+ s.description = "This plugin adds functions to support project meetings to OpenProject. Meetings
+ can be scheduled selecting invitees from the same project to take part in the meeting. An agenda
+ can be created and sent to the invitees. After the meeting, attendees can be selected and
+ minutes can be created based on the agenda. Finally, the minutes can be sent to all attendees
+ and invitees."
+ s.license = 'GPLv3'
+
+ s.files = Dir['{app,config,db,lib,doc}/**/*', 'README.md']
+ s.test_files = Dir['spec/**/*']
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_development_dependency 'factory_girl_rails', '~> 4.0'
+end
diff --git a/vendored-plugins/openproject-meeting/spec/controllers/meeting_agendas_controller_spec.rb b/vendored-plugins/openproject-meeting/spec/controllers/meeting_agendas_controller_spec.rb
new file mode 100644
index 0000000000..53aaaae1c3
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/controllers/meeting_agendas_controller_spec.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MeetingAgendasController, type: :controller do
+ let(:meeting) { FactoryGirl.create(:meeting) }
+ let(:user) { FactoryGirl.create(:admin) }
+
+ before { allow(User).to receive(:current).and_return(user) }
+
+ describe 'preview' do
+ let(:text) { 'Meeting agenda content' }
+
+ it_behaves_like 'valid preview' do
+ let(:preview_texts) { [text] }
+ let(:preview_params) { { meeting_id: meeting.id, meeting_agenda: { text: text } } }
+ end
+
+ it_behaves_like 'authorizes object access' do
+ let(:preview_params) { { meeting_id: meeting.id, meeting_agenda: {} } }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/controllers/meeting_contents_controller_spec.rb b/vendored-plugins/openproject-meeting/spec/controllers/meeting_contents_controller_spec.rb
new file mode 100644
index 0000000000..6a2ad1fb4b
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/controllers/meeting_contents_controller_spec.rb
@@ -0,0 +1,95 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'spec_helper'
+
+describe MeetingContentsController do
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_meetings]) }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:author) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:watcher1) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:watcher2) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:meeting) { FactoryGirl.create(:meeting, author: author, project: project) }
+ let(:meeting_agenda) do
+ FactoryGirl.create(:meeting_agenda, meeting: meeting)
+ end
+
+ before(:each) do
+ ActionMailer::Base.deliveries = []
+ allow_any_instance_of(MeetingContentsController).to receive(:find_content)
+ allow(controller).to receive(:authorize)
+ meeting.participants.merge([meeting.participants.build(user: watcher1, invited: true, attended: false),
+ meeting.participants.build(user: watcher2, invited: true, attended: false)])
+ meeting.save!
+ controller.instance_variable_set(:@content, meeting_agenda.meeting.agenda)
+ controller.instance_variable_set(:@content_type, 'meeting_agenda')
+ end
+
+ shared_examples_for 'delivered by mail' do
+ before { put 'notify', meeting_id: meeting.id }
+
+ it { expect(ActionMailer::Base.deliveries.count).to eql(mail_count) }
+ end
+
+ describe 'PUT' do
+ describe 'notify' do
+ context 'when author no_self_notified property is true' do
+ before do
+ author.pref[:no_self_notified] = true
+ author.save!
+ end
+
+ it_behaves_like 'delivered by mail' do
+ let(:mail_count) { 2 }
+ end
+ end
+
+ context 'when author no_self_notified property is false' do
+ before do
+ author.pref[:no_self_notified] = false
+ author.save!
+ end
+
+ it_behaves_like 'delivered by mail' do
+ let(:mail_count) { 3 }
+ end
+ end
+
+ context 'with an error during deliver' do
+ before do
+ author.pref[:no_self_notified] = false
+ author.save!
+ allow(MeetingMailer).to receive(:content_for_review).and_raise(Net::SMTPError)
+ end
+
+ it 'does not raise an error' do
+ expect { put 'notify', meeting_id: meeting.id }.to_not raise_error
+ end
+
+ it 'produces a flash message containing the mail addresses raising the error' do
+ put 'notify', meeting_id: meeting.id
+ meeting.participants.each do |participant|
+ expect(flash[:error]).to include(participant.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/controllers/meeting_minutes_controller_spec.rb b/vendored-plugins/openproject-meeting/spec/controllers/meeting_minutes_controller_spec.rb
new file mode 100644
index 0000000000..d9b8dcc22d
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/controllers/meeting_minutes_controller_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MeetingMinutesController, type: :controller do
+ let(:meeting) { FactoryGirl.create(:meeting) }
+ let(:user) { FactoryGirl.create(:admin) }
+
+ before { allow(User).to receive(:current).and_return(user) }
+
+ describe 'preview' do
+ let(:text) { 'Meeting minutes content' }
+
+ before { allow_any_instance_of(MeetingMinutes).to receive(:editable?).and_return(true) }
+
+ it_behaves_like 'valid preview' do
+ let(:preview_texts) { [text] }
+ let(:preview_params) { { meeting_id: meeting.id, meeting_minutes: { text: text } } }
+ end
+
+ it_behaves_like 'authorizes object access' do
+ let(:preview_params) { { meeting_id: meeting.id, meeting_minutes: {} } }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/controllers/meetings_controller_spec.rb b/vendored-plugins/openproject-meeting/spec/controllers/meetings_controller_spec.rb
new file mode 100644
index 0000000000..69573cc2c5
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/controllers/meetings_controller_spec.rb
@@ -0,0 +1,99 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MeetingsController, type: :controller do
+ before(:each) do
+ @p = mock_model(Project)
+ allow(@controller).to receive(:authorize)
+ allow(@controller).to receive(:check_if_login_required)
+ end
+
+ describe 'GET' do
+ describe 'index' do
+ before(:each) do
+ allow(Project).to receive(:find).and_return(@p)
+ @ms = [mock_model(Meeting), mock_model(Meeting), mock_model(Meeting)]
+ allow(@ms).to receive(:from_tomorrow).and_return(@ms)
+ allow(@p).to receive(:meetings).and_return(@ms)
+ [:with_users_by_date, :page, :per_page].each do |meth|
+ expect(@ms).to receive(meth).and_return(@ms)
+ end
+ @grouped = double('grouped')
+ expect(Meeting).to receive(:group_by_time).with(@ms).and_return(@grouped)
+ end
+ describe 'html' do
+ before(:each) do
+ get 'index', project_id: @p.id
+ end
+ it { expect(response).to be_success }
+ it { expect(assigns(:meetings_by_start_year_month_date)).to eql @grouped }
+ end
+ end
+
+ describe 'show' do
+ before(:each) do
+ @m = mock_model(Meeting)
+ allow(Meeting).to receive_message_chain(:includes, :find).and_return(@m)
+ allow(@m).to receive(:project).and_return(@p)
+ allow(allow(@m).to receive(:agenda)).to receive(:present?).and_return(false)
+ end
+ describe 'html' do
+ before(:each) do
+ get 'show', id: @m.id
+ end
+ it { expect(response).to be_success }
+ end
+ end
+
+ describe 'new' do
+ before(:each) do
+ allow(Project).to receive(:find).and_return(@p)
+ @m = mock_model(Meeting)
+ allow(@m).to receive(:project=)
+ allow(@m).to receive(:author=)
+ allow(Meeting).to receive(:new).and_return(@m)
+ end
+ describe 'html' do
+ before(:each) do
+ get 'new', project_id: @p.id
+ end
+ it { expect(response).to be_success }
+ it { expect(assigns(:meeting)).to eql @m }
+ end
+ end
+
+ describe 'edit' do
+ before(:each) do
+ @m = mock_model(Meeting)
+ allow(Meeting).to receive_message_chain(:includes, :find).and_return(@m)
+ allow(@m).to receive(:project).and_return(@p)
+ end
+ describe 'html' do
+ before(:each) do
+ get 'edit', id: @m.id
+ end
+ it { expect(response).to be_success }
+ it { expect(assigns(:meeting)).to eql @m }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_agenda_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_agenda_factory.rb
new file mode 100644
index 0000000000..9c3eaa502f
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_agenda_factory.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :meeting_agenda do |_a|
+ meeting
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_content_journal_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_content_journal_factory.rb
new file mode 100644
index 0000000000..e8696f8ef7
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_content_journal_factory.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :journal_meeting_content_journal, class: Journal::MeetingContentJournal do
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_factory.rb
new file mode 100644
index 0000000000..8f4244dfd5
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_factory.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :meeting do |m|
+ author factory: :user
+ project
+ m.sequence(:title) { |n| "Meeting #{n}" }
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_journal_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_journal_factory.rb
new file mode 100644
index 0000000000..253f827fac
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_journal_factory.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :meeting_journal do
+ created_at Time.now
+ sequence(:version) { |n| n }
+
+ factory :meeting_content_journal, class: Journal do
+ journable_type 'MeetingContent'
+ activity_type 'meetings'
+ data FactoryGirl.build(:journal_meeting_content_journal)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_minutes_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_minutes_factory.rb
new file mode 100644
index 0000000000..17e8085418
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_minutes_factory.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :meeting_minutes do |_m|
+ meeting
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/factories/meeting_participant_factory.rb b/vendored-plugins/openproject-meeting/spec/factories/meeting_participant_factory.rb
new file mode 100644
index 0000000000..94577253a3
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/factories/meeting_participant_factory.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+FactoryGirl.define do
+ factory :meeting_participant do |_mp|
+ user
+ meeting
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/lib/open_project/meeting/default_data_spec.rb b/vendored-plugins/openproject-meeting/spec/lib/open_project/meeting/default_data_spec.rb
new file mode 100644
index 0000000000..2373ec21ef
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/lib/open_project/meeting/default_data_spec.rb
@@ -0,0 +1,64 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe OpenProject::Meeting::DefaultData do
+ let(:seeder) { BasicData::RoleSeeder.new }
+
+ let(:roles) { [member, reader] }
+ let(:member) { OpenProject::Meeting::DefaultData.member_role }
+ let(:reader) { OpenProject::Meeting::DefaultData.reader_role }
+
+ let(:member_permissions) { OpenProject::Meeting::DefaultData.member_permissions }
+ let(:reader_permissions) { OpenProject::Meeting::DefaultData.reader_permissions }
+
+ before do
+ allow(seeder).to receive(:builtin_roles).and_return([])
+
+ seeder.seed!
+ end
+
+ it 'adds permissions to the roles' do
+ expect(member.permissions).to include *member_permissions
+ expect(reader.permissions).to include *reader_permissions
+ end
+
+ it 'is not loaded again on existing data' do
+ roles.each do |role|
+ role.permissions.clear
+ role.save!
+ end
+
+ seeder.seed!
+ roles.each(&:reload)
+
+ expect(member.permissions).to be_empty
+ expect(reader.permissions).to be_empty
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/mailers/meeting_mailer_spec.rb b/vendored-plugins/openproject-meeting/spec/mailers/meeting_mailer_spec.rb
new file mode 100644
index 0000000000..fdee8a737e
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/mailers/meeting_mailer_spec.rb
@@ -0,0 +1,103 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MeetingMailer, type: :mailer do
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_meetings]) }
+ let(:project) { FactoryGirl.create(:project) }
+ let(:author) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:watcher1) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:watcher2) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
+ let(:meeting) { FactoryGirl.create(:meeting, author: author, project: project) }
+ let(:meeting_agenda) do
+ FactoryGirl.create(:meeting_agenda, meeting: meeting)
+ end
+
+ before(:each) do
+ author.pref[:no_self_notified] = false
+ author.save!
+ meeting.participants.merge([meeting.participants.build(user: watcher1, invited: true, attended: false),
+ meeting.participants.build(user: watcher2, invited: true, attended: false)])
+ meeting.save!
+ end
+
+ describe 'content_for_review' do
+ let(:mail) { MeetingMailer.content_for_review meeting_agenda, 'agenda', author.mail }
+ # this is needed to call module functions from Redmine::I18n
+ let(:i18n) do
+ class A
+ include Redmine::I18n
+ public :format_date, :format_time
+ end
+ A.new
+ end
+
+ it 'renders the headers' do
+ expect(mail.subject).to include(meeting.project.name)
+ expect(mail.subject).to include(meeting.title)
+ expect(mail.to).to match_array([author.mail])
+ expect(mail.from).to eq([Setting.mail_from])
+ end
+
+ it 'renders the text body' do
+ check_meeting_mail_content mail.text_part.body
+ end
+
+ it 'renders the html body' do
+ check_meeting_mail_content mail.html_part.body
+ end
+ end
+
+ def check_meeting_mail_content(body)
+ expect(body).to include(meeting.project.name)
+ expect(body).to include(meeting.title)
+ expect(body).to include(i18n.format_date meeting.start_date)
+ expect(body).to include(i18n.format_time meeting.start_time, false)
+ expect(body).to include(i18n.format_time meeting.end_time, false)
+ expect(body).to include(meeting.participants[0].name)
+ expect(body).to include(meeting.participants[1].name)
+ end
+
+ def save_and_open_mail_html_body(mail)
+ save_and_open_mail_part mail.html_part.body
+ end
+
+ def save_and_open_mail_text_body(mail)
+ save_and_open_mail_part mail.text_part.body
+ end
+
+ def save_and_open_mail_part(part)
+ FileUtils.mkdir_p(Rails.root.join('tmp/mails'))
+
+ page_path = Rails.root.join("tmp/mails/#{SecureRandom.hex(16)}.html").to_s
+ File.open(page_path, 'w') { |f| f.write(part) }
+
+ Launchy.open(page_path)
+
+ begin
+ binding.pry
+ rescue NoMethodError
+ debugger
+ end
+
+ FileUtils.rm(page_path)
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/models/meeting_agenda_spec.rb b/vendored-plugins/openproject-meeting/spec/models/meeting_agenda_spec.rb
new file mode 100644
index 0000000000..d9ad3062ec
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/models/meeting_agenda_spec.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe 'MeetingAgenda', type: :model do
+ before(:each) do
+ @a = FactoryGirl.build :meeting_agenda, text: "Some content...\n\nMore content!\n\nExtraordinary content!!"
+ end
+
+ # TODO: Test the right user and messages are set in the history
+ describe '#lock!' do
+ it 'locks the agenda' do
+ @a.save
+ @a.reload
+ @a.lock!
+ @a.reload
+ expect(@a.locked).to be_truthy
+ end
+ end
+
+ describe '#unlock!' do
+ it 'unlocks the agenda' do
+ @a.locked = true
+ @a.save
+ @a.reload
+ @a.unlock!
+ @a.reload
+ expect(@a.locked).to be_falsey
+ end
+ end
+
+ # a meeting agenda is editable when it is not locked
+ describe '#editable?' do
+ it 'is editable when not locked' do
+ @a.locked = false
+ expect(@a.editable?).to be_truthy
+ end
+ it 'is not editable when locked' do
+ @a.locked = true
+ expect(@a.editable?).to be_falsey
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/models/meeting_content_journal_spec.rb b/vendored-plugins/openproject-meeting/spec/models/meeting_content_journal_spec.rb
new file mode 100644
index 0000000000..0528381ba1
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/models/meeting_content_journal_spec.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'journal/meeting_content_journal'
+
+describe Journal, type: :model do
+ include PluginSpecHelper
+
+ let(:journal) { FactoryGirl.build(:meeting_content_journal) }
+
+ it_should_behave_like 'customized journal class'
+end
diff --git a/vendored-plugins/openproject-meeting/spec/models/meeting_minutes_spec.rb b/vendored-plugins/openproject-meeting/spec/models/meeting_minutes_spec.rb
new file mode 100644
index 0000000000..73e879d110
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/models/meeting_minutes_spec.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe 'MeetingMinutes', type: :model do
+ before do
+ @min = FactoryGirl.build :meeting_minutes
+ end
+
+ # meeting minutes are editable when the meeting agenda is locked
+ describe '#editable?' do
+ before(:each) do
+ @mee = FactoryGirl.build :meeting
+ @min.meeting = @mee
+ end
+ describe 'with no agenda present' do
+ it 'is not editable' do
+ expect(@min.editable?).to be_falsey
+ end
+ end
+ describe 'with an agenda present' do
+ before(:each) do
+ @a = FactoryGirl.build :meeting_agenda
+ @mee.agenda = @a
+ end
+ it 'is not editable when the agenda is open' do
+ expect(@min.editable?).to be_falsey
+ end
+ it 'is editable when the agenda is closed' do
+ @a.lock!
+ expect(@min.editable?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/models/meeting_spec.rb b/vendored-plugins/openproject-meeting/spec/models/meeting_spec.rb
new file mode 100644
index 0000000000..ca299f7b80
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/models/meeting_spec.rb
@@ -0,0 +1,187 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Meeting, type: :model do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to belong_to :author }
+ it { is_expected.to validate_presence_of :title }
+ it { is_expected.to validate_presence_of :start_time }
+ it { skip; is_expected.to accept_nested_attributes_for :participants } # geht das?
+
+ let(:project) { FactoryGirl.create(:project) }
+ let(:user1) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:meeting) { FactoryGirl.create(:meeting, project: project, author: user1) }
+ let(:agenda) do
+ meeting.create_agenda text: 'Meeting Agenda text'
+ meeting.agenda(true) # avoiding stale object errors
+ end
+
+ let(:role) { FactoryGirl.create(:role, permissions: [:view_meetings]) }
+
+ before do
+ @m = FactoryGirl.build :meeting, title: 'dingens'
+ end
+
+ describe 'to_s' do
+ it { expect(@m.to_s).to eq('dingens') }
+ end
+
+ describe 'start_date' do
+ it { expect(@m.start_date).to eq(Date.tomorrow) }
+ end
+
+ describe 'start_month' do
+ it { expect(@m.start_month).to eq(Date.tomorrow.month) }
+ end
+
+ describe 'start_year' do
+ it { expect(@m.start_year).to eq(Date.tomorrow.year) }
+ end
+
+ describe 'end_time' do
+ it { expect(@m.end_time).to eq(Date.tomorrow + 11.hours) }
+ end
+
+ describe 'time-sorted finder' do
+ it { skip }
+ end
+
+ describe 'Journalized Objects' do
+ before(:each) do
+ @project ||= FactoryGirl.create(:project_with_types)
+ @current = FactoryGirl.create(:user, login: 'user1', mail: 'user1@users.com')
+ allow(User).to receive(:current).and_return(@current)
+ end
+
+ it 'should work with meeting' do
+ @meeting ||= FactoryGirl.create(:meeting, title: 'Test', project: @project, author: @current)
+
+ initial_journal = @meeting.journals.first
+ recreated_journal = @meeting.recreate_initial_journal!
+ expect(initial_journal.identical?(recreated_journal)).to be true
+ end
+ end
+
+ describe 'all_changeable_participants' do
+ describe 'WITH a user having the view_meetings permission' do
+ before do
+ project.add_member user1, [role]
+ project.save!
+ end
+
+ it 'should contain the user' do
+ expect(meeting.all_changeable_participants).to eq([user1])
+ end
+ end
+
+ describe 'WITH a user not having the view_meetings permission' do
+ let(:role2) { FactoryGirl.create(:role, permissions: []) }
+
+ before do
+ # adding both users so that the author is valid
+ project.add_member user1, [role]
+ project.add_member user2, [role2]
+
+ project.save!
+ end
+
+ it 'should not contain the user' do
+ expect(meeting.all_changeable_participants.include?(user2)).to be_falsey
+ end
+ end
+
+ describe 'WITH a user being locked but invited' do
+ let(:locked_user) { FactoryGirl.create(:locked_user) }
+ before do
+ meeting.participants_attributes = [{ 'user_id' => locked_user.id, 'invited' => 1 }]
+ end
+
+ it 'should contain the user' do
+ expect(meeting.all_changeable_participants.include?(locked_user)).to be_truthy
+ end
+ end
+ end
+
+ describe 'participants and author as watchers' do
+ before do
+ project.add_member user1, [role]
+ project.add_member user2, [role]
+
+ project.save!
+
+ meeting.participants.build(user: user2)
+ meeting.save!
+ end
+
+ it { expect(meeting.watchers.collect(&:user)).to match_array([user1, user2]) }
+ end
+
+ describe '#close_agenda_and_copy_to_minutes' do
+ before do
+ agenda # creating it
+
+ meeting.close_agenda_and_copy_to_minutes!
+ end
+
+ it "should create a meeting with the agenda's text" do
+ expect(meeting.minutes.text).to eq(meeting.agenda.text)
+ end
+
+ it 'should close the agenda' do
+ expect(meeting.agenda.locked?).to be_truthy
+ end
+ end
+
+ describe 'Copied meetings' do
+ before do
+ project.add_member user1, [role]
+ project.add_member user2, [role]
+
+ project.save!
+
+ meeting.start_time = DateTime.new(2013, 3, 27, 15, 35)
+ meeting.participants.build(user: user2)
+ meeting.save!
+ end
+
+ it 'should have the same start_time as the original meeting' do
+ copy = meeting.copy({})
+ expect(copy.start_time).to eq(meeting.start_time)
+ end
+
+ it 'should delete the copied meeting author if no author is given as parameter' do
+ copy = meeting.copy({})
+ expect(copy.author).to be_nil
+ end
+
+ it 'should set the author to the provided author if one is given' do
+ copy = meeting.copy author: user2
+ expect(copy.author).to eq(user2)
+ end
+
+ it 'should clear participant ids and attended flags for all copied attendees' do
+ copy = meeting.copy({})
+ expect(copy.participants.all? { |p| p.id.nil? && !p.attended }).to be_truthy
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/models/user_deletion_spec.rb b/vendored-plugins/openproject-meeting/spec/models/user_deletion_spec.rb
new file mode 100644
index 0000000000..0f9179f257
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/models/user_deletion_spec.rb
@@ -0,0 +1,201 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe User, '#destroy', type: :model do
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:substitute_user) { DeletedUser.first }
+ let(:project) do
+ project = FactoryGirl.create(:valid_project)
+ project
+ end
+
+ let(:meeting) {
+ FactoryGirl.create(:meeting, project: project,
+ author: user2)
+ }
+ let(:participant) {
+ FactoryGirl.create(:meeting_participant, user: user,
+ meeting: meeting,
+ invited: true,
+ attended: true)
+ }
+
+ before do
+ user
+ user2
+ end
+
+ shared_examples_for 'updated journalized associated object' do
+ before do
+ allow(User).to receive(:current).and_return(user2)
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user2)
+ end
+ associated_instance.save!
+
+ allow(User).to receive(:current).and_return(user) # in order to have the content journal created by the user
+ associated_instance.reload
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user)
+ end
+ associated_instance.save!
+
+ user.destroy
+ associated_instance.reload
+ end
+
+ it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) }
+ it 'should replace the user on all associations' do
+ associations.each do |association|
+ expect(associated_instance.send(association)).to eq(substitute_user)
+ end
+ end
+ it { expect(associated_instance.journals.first.user).to eq(user2) }
+ it 'should update first journal changes' do
+ associations.each do |association|
+ expect(associated_instance.journals.first.changed_data[(association.to_s + '_id').to_sym].last).to eq(user2.id)
+ end
+ end
+ it { expect(associated_instance.journals.last.user).to eq(substitute_user) }
+ it 'should update second journal changes' do
+ associations.each do |association|
+ expect(associated_instance.journals.last.changed_data[(association.to_s + '_id').to_sym].last).to eq(substitute_user.id)
+ end
+ end
+ end
+
+ shared_examples_for 'created journalized associated object' do
+ before do
+ allow(User).to receive(:current).and_return(user) # in order to have the content journal created by the user
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user)
+ end
+ associated_instance.save!
+
+ allow(User).to receive(:current).and_return(user2)
+ associated_instance.reload
+ associations.each do |association|
+ associated_instance.send(association.to_s + '=', user2)
+ end
+ associated_instance.save!
+
+ user.destroy
+ associated_instance.reload
+ end
+
+ it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) }
+ it 'should keep the current user on all associations' do
+ associations.each do |association|
+ expect(associated_instance.send(association)).to eq(user2)
+ end
+ end
+ it { expect(associated_instance.journals.first.user).to eq(substitute_user) }
+ it 'should update the first journal' do
+ associations.each do |association|
+ expect(associated_instance.journals.first.changed_data[(association.to_s + '_id').to_sym].last).to eq(substitute_user.id)
+ end
+ end
+ it { expect(associated_instance.journals.last.user).to eq(user2) }
+ it 'should update the last journal' do
+ associations.each do |association|
+ expect(associated_instance.journals.last.changed_data[(association.to_s + '_id').to_sym].first).to eq(substitute_user.id)
+ expect(associated_instance.journals.last.changed_data[(association.to_s + '_id').to_sym].last).to eq(user2.id)
+ end
+ end
+ end
+
+ describe 'WHEN the user created a meeting' do
+ let(:associations) { [:author] }
+ let(:associated_instance) { FactoryGirl.build(:meeting, project: project) }
+ let(:associated_class) { Meeting }
+
+ it_should_behave_like 'created journalized associated object'
+ end
+
+ describe 'WHEN the user updated a meeting' do
+ let(:associations) { [:author] }
+ let(:associated_instance) { FactoryGirl.build(:meeting, project: project) }
+ let(:associated_class) { Meeting }
+
+ it_should_behave_like 'updated journalized associated object'
+ end
+
+ describe 'WHEN the user created a meeting agenda' do
+ let(:associations) { [:author] }
+ let(:associated_instance) {
+ FactoryGirl.build(:meeting_agenda, meeting: meeting,
+ text: 'lorem')
+ }
+ let(:associated_class) { MeetingAgenda }
+
+ it_should_behave_like 'created journalized associated object'
+ end
+
+ describe 'WHEN the user updated a meeting agenda' do
+ let(:associations) { [:author] }
+ let(:associated_instance) {
+ FactoryGirl.build(:meeting_agenda, meeting: meeting,
+ text: 'lorem')
+ }
+ let(:associated_class) { MeetingAgenda }
+
+ it_should_behave_like 'updated journalized associated object'
+ end
+
+ describe 'WHEN the user created a meeting minutes' do
+ let(:associations) { [:author] }
+ let(:associated_instance) {
+ FactoryGirl.build(:meeting_minutes, meeting: meeting,
+ text: 'lorem')
+ }
+ let(:associated_class) { MeetingMinutes }
+
+ it_should_behave_like 'created journalized associated object'
+ end
+
+ describe 'WHEN the user updated a meeting minutes' do
+ let(:associations) { [:author] }
+ let(:associated_instance) {
+ FactoryGirl.build(:meeting_minutes, meeting: meeting,
+ text: 'lorem')
+ }
+ let(:associated_class) { MeetingMinutes }
+
+ it_should_behave_like 'updated journalized associated object'
+ end
+
+ describe 'WHEN the user participated in a meeting' do
+ before do
+ participant
+ # user2 added to participants by beeing the author
+
+ user.destroy
+ meeting.reload
+ participant.reload
+ end
+
+ it { expect(meeting.participants.map(&:user)).to match_array([DeletedUser.first, user2]) }
+ it { expect(participant.invited).to be_truthy }
+ it { expect(participant.attended).to be_truthy }
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/routing/previews_routing_spec.rb b/vendored-plugins/openproject-meeting/spec/routing/previews_routing_spec.rb
new file mode 100644
index 0000000000..7fc3f6d8d6
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/routing/previews_routing_spec.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe 'preview', type: :routing do
+ it 'should connect POST /meetings/:meeting_id/agenda/preview to meeting_agendas#preview' do
+ expect(post('/meetings/1/agenda/preview')).to route_to(controller: 'meeting_agendas',
+ meeting_id: '1',
+ action: 'preview')
+ end
+
+ it 'should connect POST /meetings/:meeting_id/agenda/preview to meeting_minutes#preview' do
+ expect(post('/meetings/1/minutes/preview')).to route_to(controller: 'meeting_minutes',
+ meeting_id: '1',
+ action: 'preview')
+ end
+end
diff --git a/vendored-plugins/openproject-meeting/spec/spec_helper.rb b/vendored-plugins/openproject-meeting/spec/spec_helper.rb
new file mode 100644
index 0000000000..75f9194a35
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+# -- load spec_helper from OpenProject core
+require 'spec_helper'
+require File.dirname(__FILE__) + '/support/plugin_spec_helper'
diff --git a/vendored-plugins/openproject-meeting/spec/support/plugin_spec_helper.rb b/vendored-plugins/openproject-meeting/spec/support/plugin_spec_helper.rb
new file mode 100644
index 0000000000..eb677ab2c0
--- /dev/null
+++ b/vendored-plugins/openproject-meeting/spec/support/plugin_spec_helper.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# OpenProject Meeting Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module PluginSpecHelper
+ shared_examples_for 'customized journal class' do
+ describe '#save' do
+ let(:text) { 'Lorem ipsum' }
+ let(:changed_data) { { text: [nil, text] } }
+
+ describe 'WITHOUT compression' do
+ before do
+ # we have to save here because changed_data will update (and save) attributes and miss an ID
+ journal.save!
+ journal.changed_data = changed_data
+
+ journal.reload
+ end
+
+ it { expect(journal.changed_data[:text][1]).to eq(text) }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/.rubocop.yml b/vendored-plugins/openproject-my_project_page/.rubocop.yml
new file mode 100644
index 0000000000..d32efc64ee
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/.rubocop.yml
@@ -0,0 +1,1007 @@
+Style/AccessModifierIndentation:
+ Description: Check indentation of private/protected visibility modifiers.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected'
+ Enabled: true
+
+Style/AccessorMethodName:
+ Description: Check the naming of accessor methods for get_/set_.
+ Enabled: false
+
+Style/Alias:
+ Description: 'Use alias_method instead of alias.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
+ Enabled: true
+
+Style/AlignArray:
+ Description: >-
+ Align the elements of an array literal if they span more than
+ one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
+ Enabled: true
+
+Style/AlignHash:
+ Description: >-
+ Align the elements of a hash literal if they span more than
+ one line.
+ Enabled: true
+
+Style/AlignParameters:
+ Description: >-
+ Align the parameters of a method call if they span more
+ than one line.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
+ Enabled: false
+
+Style/AndOr:
+ Description: 'Use &&/|| instead of and/or.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or'
+ Enabled: false
+
+Style/ArrayJoin:
+ Description: 'Use Array#join instead of Array#*.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join'
+ Enabled: false
+
+Style/AsciiComments:
+ Description: 'Use only ascii symbols in comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
+ Enabled: false
+
+Style/AsciiIdentifiers:
+ Description: 'Use only ascii symbols in identifiers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
+ Enabled: true
+
+Style/Attr:
+ Description: 'Checks for uses of Module#attr.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr'
+ Enabled: false
+
+Style/BeginBlock:
+ Description: 'Avoid the use of BEGIN blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks'
+ Enabled: true
+
+Style/BarePercentLiterals:
+ Description: 'Checks if usage of %() or %Q() matches configuration.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand'
+ Enabled: false
+
+Style/BlockComments:
+ Description: 'Do not use block comments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments'
+ Enabled: false
+
+Style/BlockDelimiters:
+ Description: >-
+ Avoid using {...} for multi-line blocks (multiline chaining is
+ always ugly).
+ Prefer {...} over do...end for single-line blocks.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/BlockEndNewline:
+ Description: 'Put end statement of multiline block on its own line.'
+ Enabled: true
+
+Style/BracesAroundHashParameters:
+ Description: 'Enforce braces style around hash parameters.'
+ Enabled: false
+
+Style/CaseEquality:
+ Description: 'Avoid explicit use of the case equality operator(===).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
+ Enabled: false
+
+Style/CaseIndentation:
+ Description: 'Indentation of when in a case/when/[else/]end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case'
+ Enabled: true
+
+Style/CharacterLiteral:
+ Description: 'Checks for uses of character literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals'
+ Enabled: true
+
+Style/ClassAndModuleCamelCase:
+ Description: 'Use CamelCase for classes and modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes'
+ Enabled: true
+
+Style/ClassAndModuleChildren:
+ Description: 'Checks style of children classes and modules.'
+ Enabled: false
+
+Style/ClassCheck:
+ Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.'
+ Enabled: false
+
+Style/ClassMethods:
+ Description: 'Use self when defining module/class methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-singletons'
+ Enabled: false
+
+Style/ClassVars:
+ Description: 'Avoid the use of class variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars'
+ Enabled: true
+
+Style/ColonMethodCall:
+ Description: 'Do not use :: for method call.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons'
+ Enabled: false
+
+Style/CommentAnnotation:
+ Description: >-
+ Checks formatting of special comments
+ (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords'
+ Enabled: false
+
+Style/CommentIndentation:
+ Description: 'Indentation of comments.'
+ Enabled: true
+
+Style/ConstantName:
+ Description: 'Constants should use SCREAMING_SNAKE_CASE.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case'
+ Enabled: true
+
+Style/DefWithParentheses:
+ Description: 'Use def with parentheses when there are arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/DeprecatedHashMethods:
+ Description: 'Checks for use of deprecated Hash methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
+ Enabled: false
+
+Style/Documentation:
+ Description: 'Document classes and non-namespace modules.'
+ Enabled: false
+
+Style/DotPosition:
+ Description: 'Checks the position of the dot in multi-line method calls.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains'
+ Enabled: false
+
+Style/DoubleNegation:
+ Description: 'Checks for uses of double negation (!!).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang'
+ Enabled: false
+
+Style/EachWithObject:
+ Description: 'Prefer `each_with_object` over `inject` or `reduce`.'
+ Enabled: false
+
+Style/ElseAlignment:
+ Description: 'Align elses and elsifs correctly.'
+ Enabled: true
+
+Style/EmptyElse:
+ Description: 'Avoid empty else-clauses.'
+ Enabled: false
+
+Style/EmptyLineBetweenDefs:
+ Description: 'Use empty lines between defs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods'
+ Enabled: false
+
+Style/EmptyLines:
+ Description: "Don't use several empty lines in a row."
+ Enabled: false
+
+Style/EmptyLinesAroundAccessModifier:
+ Description: "Keep blank lines around access modifiers."
+ Enabled: false
+
+Style/EmptyLinesAroundBlockBody:
+ Description: "Keeps track of empty lines around block bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundClassBody:
+ Description: "Keeps track of empty lines around class bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundModuleBody:
+ Description: "Keeps track of empty lines around module bodies."
+ Enabled: false
+
+Style/EmptyLinesAroundMethodBody:
+ Description: "Keeps track of empty lines around method bodies."
+ Enabled: false
+
+Style/EmptyLiteral:
+ Description: 'Prefer literals to Array.new/Hash.new/String.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash'
+ Enabled: false
+
+Style/EndBlock:
+ Description: 'Avoid the use of END blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks'
+ Enabled: false
+
+Style/EndOfLine:
+ Description: 'Use Unix-style line endings.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
+ Enabled: false
+
+Style/EvenOdd:
+ Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: false
+
+Style/FileName:
+ Description: 'Use snake_case for source file names.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
+ Enabled: false
+
+Style/FlipFlop:
+ Description: 'Checks for flip flops'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
+ Enabled: false
+
+Style/For:
+ Description: 'Checks use of for or each in multiline loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops'
+ Enabled: false
+
+Style/FormatString:
+ Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
+ Enabled: false
+
+Style/GlobalVars:
+ Description: 'Do not introduce global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
+ Enabled: false
+
+Style/GuardClause:
+ Description: 'Check for conditionals that can be replaced with guard clauses'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/HashSyntax:
+ Description: >-
+ Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax
+ { :a => 1, :b => 2 }.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals'
+ Enabled: true
+
+Style/IfUnlessModifier:
+ Description: >-
+ Favor modifier if/unless usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier'
+ Enabled: false
+
+Style/IfWithSemicolon:
+ Description: 'Do not use if x; .... Use the ternary operator instead.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs'
+ Enabled: false
+
+Style/IndentationConsistency:
+ Description: 'Keep indentation straight.'
+ Enabled: true
+
+Style/IndentationWidth:
+ Description: 'Use 2 spaces for indentation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/IndentArray:
+ Description: >-
+ Checks the indentation of the first element in an array
+ literal.
+ Enabled: false
+
+Style/IndentHash:
+ Description: 'Checks the indentation of the first key in a hash literal.'
+ Enabled: false
+
+Style/InfiniteLoop:
+ Description: 'Use Kernel#loop for infinite loops.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop'
+ Enabled: false
+
+Style/Lambda:
+ Description: 'Use the new lambda literal syntax for single-line blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line'
+ Enabled: false
+
+Style/LambdaCall:
+ Description: 'Use lambda.call(...) instead of lambda.(...).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call'
+ Enabled: false
+
+Style/LeadingCommentSpace:
+ Description: 'Comments should start with a space.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
+ Enabled: false
+
+Style/LineEndConcatenation:
+ Description: >-
+ Use \ instead of + or << to concatenate two string literals at
+ line end.
+ Enabled: false
+
+Style/MethodCallParentheses:
+ Description: 'Do not use parentheses for method calls with no arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
+ Enabled: false
+
+Style/MethodDefParentheses:
+ Description: >-
+ Checks if the method definitions have or don't have
+ parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
+ Enabled: false
+
+Style/MethodName:
+ Description: 'Use the configured style when naming methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/ModuleFunction:
+ Description: 'Checks for usage of `extend self` in modules.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function'
+ Enabled: false
+
+Style/MultilineBlockChain:
+ Description: 'Avoid multi-line chains of blocks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
+ Enabled: false
+
+Style/MultilineBlockLayout:
+ Description: 'Ensures newlines after multiline block do statements.'
+ Enabled: true
+
+Style/MultilineIfThen:
+ Description: 'Do not use then for multi-line if/unless.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then'
+ Enabled: false
+
+Style/MultilineOperationIndentation:
+ Description: >-
+ Checks indentation of binary operations that span more than
+ one line.
+ Enabled: false
+
+Style/MultilineTernaryOperator:
+ Description: >-
+ Avoid multi-line ?: (the ternary operator);
+ use if/unless instead.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary'
+ Enabled: false
+
+Style/NegatedIf:
+ Description: >-
+ Favor unless over if for negative conditions
+ (or control flow or).
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives'
+ Enabled: false
+
+Style/NegatedWhile:
+ Description: 'Favor until over while for negative conditions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives'
+ Enabled: false
+
+Style/NestedTernaryOperator:
+ Description: 'Use one expression per branch in a ternary operator.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
+ Enabled: true
+
+Style/Next:
+ Description: 'Use `next` to skip iteration instead of a condition at the end.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals'
+ Enabled: false
+
+Style/NilComparison:
+ Description: 'Prefer x.nil? to x == nil.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
+ Enabled: true
+
+Style/NonNilCheck:
+ Description: 'Checks for redundant nil checks.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
+ Enabled: true
+
+Style/Not:
+ Description: 'Use ! instead of not.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
+ Enabled: true
+
+Style/NumericLiterals:
+ Description: >-
+ Add underscores to large numeric literals to improve their
+ readability.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics'
+ Enabled: false
+
+Style/OneLineConditional:
+ Description: >-
+ Favor the ternary operator(?:) over
+ if/then/else/end constructs.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
+ Enabled: true
+
+Style/OpMethod:
+ Description: 'When defining binary operators, name the argument other.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
+ Enabled: false
+
+Style/ParenthesesAroundCondition:
+ Description: >-
+ Don't use parentheses around the condition of an
+ if/unless/while.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
+ Enabled: true
+
+Style/PercentLiteralDelimiters:
+ Description: 'Use `%`-literal delimiters consistently'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces'
+ Enabled: false
+
+Style/PercentQLiterals:
+ Description: 'Checks if uses of %Q/%q match the configured preference.'
+ Enabled: false
+
+Style/PerlBackrefs:
+ Description: 'Avoid Perl-style regex back references.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
+ Enabled: false
+
+Style/PredicateName:
+ Description: 'Check the names of predicate methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
+ Enabled: false
+
+Style/Proc:
+ Description: 'Use proc instead of Proc.new.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc'
+ Enabled: false
+
+Style/RaiseArgs:
+ Description: 'Checks the arguments passed to raise/fail.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages'
+ Enabled: false
+
+Style/RedundantBegin:
+ Description: "Don't use begin blocks when they are not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit'
+ Enabled: false
+
+Style/RedundantException:
+ Description: "Checks for an obsolete RuntimeException argument in raise/fail."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror'
+ Enabled: false
+
+Style/RedundantReturn:
+ Description: "Don't use return where it's not required."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
+ Enabled: true
+
+Style/RedundantSelf:
+ Description: "Don't use self where it's not needed."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required'
+ Enabled: false
+
+Style/RegexpLiteral:
+ Description: >-
+ Use %r for regular expressions matching more than
+ `MaxSlashes` '/' characters.
+ Use %r only for regular expressions matching more than
+ `MaxSlashes` '/' character.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
+ Enabled: false
+
+Style/RescueModifier:
+ Description: 'Avoid using rescue in its modifier form.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers'
+ Enabled: false
+
+Style/SelfAssignment:
+ Description: >-
+ Checks for places where self-assignment shorthand should have
+ been used.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment'
+ Enabled: false
+
+Style/Semicolon:
+ Description: "Don't use semicolons to terminate expressions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon'
+ Enabled: false
+
+Style/SignalException:
+ Description: 'Checks for proper usage of fail and raise.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method'
+ Enabled: false
+
+Style/SingleLineBlockParams:
+ Description: 'Enforces the names of some block params.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
+ Enabled: false
+
+Style/SingleLineMethods:
+ Description: 'Avoid single-line methods.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods'
+ Enabled: false
+
+Style/SingleSpaceBeforeFirstArg:
+ Description: >-
+ Checks that exactly one space is used between a method name
+ and the first argument for method calls without parentheses.
+ Enabled: false
+
+Style/SpaceAfterColon:
+ Description: 'Use spaces after colons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterComma:
+ Description: 'Use spaces after commas.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceAfterControlKeyword:
+ Description: 'Use spaces after if/elsif/unless/while/until/case/when.'
+ Enabled: false
+
+Style/SpaceAfterMethodName:
+ Description: >-
+ Do not put a space between a method name and the opening
+ parenthesis in a method definition.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: false
+
+Style/SpaceAfterNot:
+ Description: Tracks redundant space after the ! operator.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang'
+ Enabled: false
+
+Style/SpaceAfterSemicolon:
+ Description: 'Use spaces after semicolons.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeBlockBraces:
+ Description: >-
+ Checks that the left block brace has or doesn't have space
+ before it.
+ Enabled: false
+
+Style/SpaceBeforeComma:
+ Description: 'No spaces before commas.'
+ Enabled: false
+
+Style/SpaceBeforeComment:
+ Description: >-
+ Checks for missing space between code and a comment on the
+ same line.
+ Enabled: false
+
+Style/SpaceBeforeSemicolon:
+ Description: 'No spaces before semicolons.'
+ Enabled: false
+
+Style/SpaceInsideBlockBraces:
+ Description: >-
+ Checks that block braces have or don't have surrounding space.
+ For blocks taking parameters, checks that the left brace has
+ or doesn't have trailing space.
+ Enabled: false
+
+Style/SpaceAroundEqualsInParameterDefault:
+ Description: >-
+ Checks that the equals signs in parameter default assignments
+ have or don't have surrounding space depending on
+ configuration.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals'
+ Enabled: false
+
+Style/SpaceAroundOperators:
+ Description: 'Use spaces around operators.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: false
+
+Style/SpaceBeforeModifierKeyword:
+ Description: 'Put a space before the modifier keyword.'
+ Enabled: false
+
+Style/SpaceInsideBrackets:
+ Description: 'No spaces after [ or before ].'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideHashLiteralBraces:
+ Description: "Use spaces inside hash literal braces - or don't."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators'
+ Enabled: true
+
+Style/SpaceInsideParens:
+ Description: 'No spaces after ( or before ).'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
+ Enabled: false
+
+Style/SpaceInsideRangeLiteral:
+ Description: 'No spaces inside range literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals'
+ Enabled: false
+
+Style/SpecialGlobalVars:
+ Description: 'Avoid Perl-style global variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms'
+ Enabled: false
+
+Style/StringLiterals:
+ Description: 'Checks if uses of quotes match the configured preference.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
+ Enabled: false
+
+Style/StringLiteralsInInterpolation:
+ Description: >-
+ Checks if uses of quotes inside expressions in interpolated
+ strings match the configured preference.
+ Enabled: false
+
+Style/StructInheritance:
+ Enabled: false
+
+Style/SymbolProc:
+ Description: 'Use symbols as procs instead of blocks when possible.'
+ Enabled: false
+
+Style/Tab:
+ Description: 'No hard tabs.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
+ Enabled: true
+
+Style/TrailingBlankLines:
+ Description: 'Checks trailing blank lines and final newline.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
+ Enabled: true
+
+Style/TrailingComma:
+ Description: 'Checks for trailing comma in parameter lists and literals.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
+ Enabled: false
+
+Style/TrailingWhitespace:
+ Description: 'Avoid trailing whitespace.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
+ Enabled: false
+
+Style/TrivialAccessors:
+ Description: 'Prefer attr_* methods to trivial readers/writers.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
+ Enabled: false
+
+Style/UnlessElse:
+ Description: >-
+ Do not use unless with else. Rewrite these with the positive
+ case first.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
+ Enabled: false
+
+Style/UnneededCapitalW:
+ Description: 'Checks for %W when interpolation is not needed.'
+ Enabled: false
+
+Style/UnneededPercentQ:
+ Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
+ Enabled: false
+
+Style/VariableInterpolation:
+ Description: >-
+ Don't interpolate global, instance and class variables
+ directly in strings.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate'
+ Enabled: false
+
+Style/VariableName:
+ Description: 'Use the configured style when naming variables.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars'
+ Enabled: false
+
+Style/WhenThen:
+ Description: 'Use when x then ... for one-line cases.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases'
+ Enabled: false
+
+Style/WhileUntilDo:
+ Description: 'Checks for redundant do after while or until.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do'
+ Enabled: false
+
+Style/WhileUntilModifier:
+ Description: >-
+ Favor modifier while/until usage when you have a
+ single-line body.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier'
+ Enabled: false
+
+Style/WordArray:
+ Description: 'Use %w or %W for arrays of words.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
+ Enabled: false
+
+#################### Metrics ################################
+
+Metrics/AbcSize:
+ Description: >-
+ A calculated magnitude based on number of assignments,
+ branches, and conditions.
+ Enabled: false
+
+Metrics/BlockNesting:
+ Description: 'Avoid excessive block nesting'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
+ Enabled: false
+
+Metrics/ClassLength:
+ Description: 'Avoid classes longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/ModuleLength:
+ Description: 'Avoid modules longer than 100 lines of code.'
+ Enabled: false
+
+Metrics/CyclomaticComplexity:
+ Description: >-
+ A complexity metric that is strongly correlated to the number
+ of test cases needed to validate a method.
+ Enabled: false
+
+Metrics/LineLength:
+ Description: 'Limit lines to 80 characters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
+ Enabled: true
+ Max: 120
+
+Metrics/MethodLength:
+ Description: 'Avoid methods longer than 10 lines of code.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
+ Enabled: false
+
+Metrics/ParameterLists:
+ Description: 'Avoid parameter lists longer than three or four parameters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
+ Enabled: false
+
+Metrics/PerceivedComplexity:
+ Description: >-
+ A complexity metric geared towards measuring complexity for a
+ human reader.
+ Enabled: false
+
+#################### Lint ################################
+### Warnings
+
+Lint/AmbiguousOperator:
+ Description: >-
+ Checks for ambiguous operators in the first argument of a
+ method invocation without parentheses.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args'
+ Enabled: false
+
+Lint/AmbiguousRegexpLiteral:
+ Description: >-
+ Checks for ambiguous regexp literals in the first argument of
+ a method invocation without parenthesis.
+ Enabled: false
+
+Lint/AssignmentInCondition:
+ Description: "Don't use assignment in conditions."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition'
+ Enabled: false
+
+Lint/BlockAlignment:
+ Description: 'Align block ends correctly.'
+ Enabled: false
+
+Lint/ConditionPosition:
+ Description: >-
+ Checks for condition placed in a confusing position relative to
+ the keyword.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition'
+ Enabled: false
+
+Lint/Debugger:
+ Description: 'Check for debugger calls.'
+ Enabled: true
+
+Lint/DefEndAlignment:
+ Description: 'Align ends corresponding to defs correctly.'
+ Enabled: false
+
+Lint/DeprecatedClassMethods:
+ Description: 'Check for deprecated class method calls.'
+ Enabled: false
+
+Lint/ElseLayout:
+ Description: 'Check for odd code arrangement in an else block.'
+ Enabled: false
+
+Lint/EmptyEnsure:
+ Description: 'Checks for empty ensure block.'
+ Enabled: false
+
+Lint/EmptyInterpolation:
+ Description: 'Checks for empty string interpolation.'
+ Enabled: false
+
+Lint/EndAlignment:
+ Description: 'Align ends correctly.'
+ Enabled: false
+
+Lint/EndInMethod:
+ Description: 'END blocks should not be placed inside method definitions.'
+ Enabled: false
+
+Lint/EnsureReturn:
+ Description: 'Do not use return in an ensure block.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
+ Enabled: false
+
+Lint/Eval:
+ Description: 'The use of eval represents a serious security risk.'
+ Enabled: false
+
+Lint/HandleExceptions:
+ Description: "Don't suppress exception."
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
+ Enabled: false
+
+Lint/InvalidCharacterLiteral:
+ Description: >-
+ Checks for invalid character literals with a non-escaped
+ whitespace character.
+ Enabled: false
+
+Lint/LiteralInCondition:
+ Description: 'Checks of literals used in conditions.'
+ Enabled: false
+
+Lint/LiteralInInterpolation:
+ Description: 'Checks for literals used in interpolation.'
+ Enabled: false
+
+Lint/Loop:
+ Description: >-
+ Use Kernel#loop with break rather than begin/end/until or
+ begin/end/while for post-loop tests.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break'
+ Enabled: false
+
+Lint/NestedMethodDefinition:
+ Description: 'Do not use nested method definitions.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods'
+ Enabled: false
+
+Lint/NonLocalExitFromIterator:
+ Description: 'Do not use return in iterator to cause non-local exit.'
+ Enabled: true
+
+Lint/ParenthesesAsGroupedExpression:
+ Description: >-
+ Checks for method calls with a space before the opening
+ parenthesis.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
+ Enabled: true
+
+Lint/RequireParentheses:
+ Description: >-
+ Use parentheses in the method call to avoid confusion
+ about precedence.
+ Enabled: false
+
+Lint/RescueException:
+ Description: 'Avoid rescuing the Exception class.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
+ Enabled: true
+
+Lint/ShadowingOuterLocalVariable:
+ Description: >-
+ Do not use the same name as outer local variable
+ for block arguments or block local variables.
+ Enabled: false
+
+Lint/SpaceBeforeFirstArg:
+ Description: >-
+ Put a space between a method name and the first argument
+ in a method call without parentheses.
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Description: 'Checks for Object#to_s usage in string interpolation.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
+ Enabled: true
+
+Lint/UnderscorePrefixedVariableName:
+ Description: 'Do not use prefix `_` for a variable that is used.'
+ Enabled: true
+
+Lint/UnusedBlockArgument:
+ Description: 'Checks for unused block arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnusedMethodArgument:
+ Description: 'Checks for unused method arguments.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UnreachableCode:
+ Description: 'Unreachable code.'
+ Enabled: true
+
+Lint/UselessAccessModifier:
+ Description: 'Checks for useless access modifiers.'
+ Enabled: false
+
+Lint/UselessAssignment:
+ Description: 'Checks for useless assignment to a local variable.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
+ Enabled: true
+
+Lint/UselessComparison:
+ Description: 'Checks for comparison of something with itself.'
+ Enabled: false
+
+Lint/UselessElseWithoutRescue:
+ Description: 'Checks for useless `else` in `begin..end` without `rescue`.'
+ Enabled: true
+
+Lint/UselessSetterCall:
+ Description: 'Checks for useless setter call to a local variable.'
+ Enabled: false
+
+Lint/Void:
+ Description: 'Possible use of operator/literal/variable in void context.'
+ Enabled: false
+
+##################### Rails ##################################
+
+Rails/ActionFilter:
+ Description: 'Enforces consistent use of action filter methods.'
+ Enabled: true
+
+Rails/DefaultScope:
+ Description: 'Checks if the argument passed to default_scope is a block.'
+ Enabled: false
+
+Rails/Delegate:
+ Description: 'Prefer delegate method for delegations.'
+ Enabled: false
+
+Rails/HasAndBelongsToMany:
+ Description: 'Prefer has_many :through to has_and_belongs_to_many.'
+ Enabled: false
+
+Rails/Output:
+ Description: 'Checks for calls to puts, print, etc.'
+ Enabled: true
+
+Rails/ReadWriteAttribute:
+ Description: >-
+ Checks for read_attribute(:attr) and
+ write_attribute(:attr, val).
+ Enabled: false
+
+Rails/ScopeArgs:
+ Description: 'Checks the arguments of ActiveRecord scopes.'
+ Enabled: false
+
+Rails/Validation:
+ Description: 'Use validates :attribute, hash of validations.'
+ Enabled: false
+
+AllCops:
+ RunRailsCops: true
+ Exclude:
+ - 'vendor/**/*'
+ - 'db/**/*'
+ - 'tmp/**/*'
+ - 'bin/**/*'
diff --git a/vendored-plugins/openproject-my_project_page/README.md b/vendored-plugins/openproject-my_project_page/README.md
new file mode 100644
index 0000000000..310176c9bb
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/README.md
@@ -0,0 +1,62 @@
+OpenProject My Project Page PlugIn
+==================================
+
+This plugin provides a customizable view of the Project-Overview-Page, very similar
+to the "My Page" in the OpenProject Core.
+
+Requirements
+------------
+
+The My Project Page plugin currently requires the OpenProject Core in version 3.0.0 or newer.
+
+
+Installation
+------------
+
+To install the My Project Page plugin, add the following line to the `Gemfile.plugins` to your OpenProject installation (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-my_project_page", :git => "https://github.com/finnlabs/openproject-my_project_page.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-my_project_page", :git => "https://github.com/finnlabs/openproject-my_project_page.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please note that this leaves plugin data in the database. Currently, we do not
+support full uninstall of the plugin.
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/my-project-page
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+
+`https://github.com/finnlabs/openproject-my_project_page`
+
+Licence
+-------
+
+(c) 2011 - 2014 - the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.rdoc and
+doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-my_project_page/app/assets/javascripts/my_project_page/my_project_page.js b/vendored-plugins/openproject-my_project_page/app/assets/javascripts/my_project_page/my_project_page.js
new file mode 100644
index 0000000000..cbcb2560cb
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/assets/javascripts/my_project_page/my_project_page.js
@@ -0,0 +1,201 @@
+//-- copyright
+// OpenProject My Project Page Plugin
+//
+// Copyright (C) 2011-2015 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.md for more details.
+//++
+
+/* globals jQuery, $$, $, Sortable, Effect, Form, Ajax, I18n, _ */
+/* jshint camelcase: false */
+/* jshint nonew: false */
+
+(function($) {
+ // $ is prototype
+ // @see app/views/my_projects_overviews/page_layout.html.erb
+
+ function recreateSortables() {
+ var lists = $$('.list-position'),
+ containedPositions = (function() {
+ var positions = _.map(lists, function(list) {
+ return list.readAttribute('id');
+ });
+ return _.uniq(positions);
+ }()),
+ destroy = function destroy(list) {
+ var id = list.readAttribute('id');
+ Sortable.destroy(id);
+ },
+ create = function create(list) {
+ var id = list.readAttribute('id'),
+ url = list.readAttribute('data-ajax-url');
+ Sortable.create(id, {
+ constraint: false,
+ dropOnEmpty: true,
+ handle: 'handle',
+ onUpdate: function updatePosition() {
+ new Ajax.Request(url, {
+ asynchronous: true,
+ evalScripts: true,
+ // this might seem like magic, but it actually
+ // replaces Sortable.serialize which breaks our
+ // block names when they contain underscores
+ parameters: (function serialize(id, $) {
+ var element = $('#' + id),
+ blocks = element.find('.widget-box').map(function() {
+ return this.id.replace(/^block_/, '');
+ }).get();
+ return blocks.map(function(item) {
+ return id + '[]=' + encodeURIComponent(item);
+ }).join('&');
+ }(id, jQuery))
+ });
+ },
+ containment: containedPositions,
+ only: 'widget-box',
+ tag: 'div'
+ });
+ };
+
+ lists.each(destroy);
+ lists.each(create);
+ }
+
+ function updateSelect() {
+ var s = $('block-select');
+ if (s === null) {
+ return;
+ }
+ for (var i = 0; i < s.options.length; i++) {
+ var name = s.options[i].value || '';
+ // this becomes necessary as the block names are saved with dashes in the db,
+ // but their ids use underscores in the frontend - this changes the name to find
+ // the block in the DOM
+ name = name.replace(/\-/g, '_');
+ if ($('block_' + name)) {
+ s.options[i].disabled = true;
+ } else {
+ s.options[i].disabled = false;
+ }
+ }
+ s.options[0].selected = true;
+ }
+
+ function afterAddBlock(response) {
+ recreateSortables();
+ updateSelect();
+ editTextilizable(extractBlockName(response));
+ new Effect.ScrollTo('list-hidden');
+ }
+
+ function extractBlockName(response) {
+ return response.responseText.match(/id="block_(.*?)"/)[1];
+ }
+
+ function resetTextilizable(name) {
+ $('textile_' + name).setValue(window['page_layout-textile' + name] + '');
+ toggleTextilizableVisibility(name);
+ return false;
+ }
+
+ function editTextilizable(name) {
+ var textile_name = $('textile_' + name);
+ if (textile_name !== null) {
+ window['page_layout-textile' + name] = textile_name.getValue();
+ toggleTextilizableVisibility(name);
+ }
+ return false;
+ }
+
+ function toggleTextilizableVisibility(name) {
+ $(name + '-form-div').toggle();
+ $(name + '-preview-div').toggle();
+ $(name + '-text').toggle();
+ }
+ function addBlock() {
+ new Ajax.Updater('list-hidden',
+ $('block-form').action,
+ { insertion: 'top',
+ onComplete: afterAddBlock,
+ parameters: Form.serialize('block-form'),
+ evalScripts:true
+ });
+
+ return false;
+ }
+
+ // prototype end
+
+ (function($) {
+ // from here on, '$' is jQuery
+
+ $(function() {
+ $('#users_per_role .all').click(function () {
+ $('#users_per_role').html('');
+ });
+
+ $.ajaxAppend({
+ trigger: '.all',
+ indicator_class: 'ajax-indicator',
+ load_target: '#users_per_role',
+ loading_text: I18n.t('js.ajax.loading'),
+ loading_class: 'box loading'
+ });
+
+ // this was previously bound in the template directly
+ $('#block-select').on('change', addBlock);
+
+ // we need to rebind some of the links constantly, as the content is generated
+ // on the page
+ function updateBlockLinks() {
+ function getBlockName(element) {
+ var blockName = element.data('block-name');
+ if (!blockName) {
+ throw new Error('no block name found for element');
+ }
+ return blockName;
+ }
+
+ // bind textilizable block links
+ $('a.reset-textilizable').on('click', function(e) {
+ e.preventDefault();
+ resetTextilizable(getBlockName($(this)));
+ });
+
+ $('a.edit-textilizable').on('click', function(e) {
+ e.preventDefault();
+ editTextilizable(getBlockName($(this)));
+ });
+ }
+
+ // initialize the fun! (prototype)
+ recreateSortables();
+ updateSelect();
+
+ // moar fun
+ updateBlockLinks();
+
+ //these are generated blocks, so we have to watch the links inside them
+
+ // TODO: this is exceptionally _not_ fun
+ // this attaches the update method to the window in order for it
+ // being callable after removal of a block
+ window.myPage = window.myPage || {
+ updateSelect: updateSelect,
+ updateBlockLinks: updateBlockLinks
+ };
+ });
+ }(jQuery));
+}($));
diff --git a/vendored-plugins/openproject-my_project_page/app/assets/stylesheets/my_project_page/my_projects_overview.sass b/vendored-plugins/openproject-my_project_page/app/assets/stylesheets/my_project_page/my_projects_overview.sass
new file mode 100644
index 0000000000..0f9d4c5579
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/assets/stylesheets/my_project_page/my_projects_overview.sass
@@ -0,0 +1,35 @@
+// -- copyright
+// OpenProject My Project Page Plugin
+//
+// Copyright (C) 2011-2015 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.md for more details.
+
+div.overview
+ padding: 6px
+ margin-bottom: 10px
+ line-height: 1.5em
+
+#content
+ .block-teaser
+ position: relative
+
+ .widget-box--header
+ margin-right: 80px
+
+ .box-actions
+ position: absolute
+ top: 20px
+ right: 20px
diff --git a/vendored-plugins/openproject-my_project_page/app/controllers/my_projects_overviews_controller.rb b/vendored-plugins/openproject-my_project_page/app/controllers/my_projects_overviews_controller.rb
new file mode 100644
index 0000000000..6955085a19
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/controllers/my_projects_overviews_controller.rb
@@ -0,0 +1,321 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MyProjectsOverviewsController < ApplicationController
+
+ menu_item :overview
+
+ before_action :find_project, :find_user
+ before_action :authorize
+ before_action :jump_to_project_menu_item, only: :index
+
+ verify xhr: true,
+ only: [:add_block, :remove_block, :order_blocks]
+
+ def self.available_blocks
+ @available_blocks ||= OpenProject::MyProjectPage.plugin_blocks
+ end
+
+ def index
+ end
+
+ # User's page layout configuration
+ def page_layout
+ end
+
+ def update_custom_element
+ block_name = params["block_name"]
+ block_title = params["block_title_#{block_name}"]
+ textile = params["textile_#{block_name}"]
+
+ if params["attachments"]
+ # Attach files and save them
+ attachments = Attachment.attach_files(overview, params["attachments"])
+ unless attachments[:unsaved].blank?
+ flash[:error] = l(:warning_attachments_not_saved, attachments[:unsaved].size)
+ end
+ end
+
+ overview.save_custom_element(block_name, block_title, textile)
+
+ redirect_to :back
+ end
+
+ # Add a block to user's page
+ # The block is added on top of the page
+ # params[:block] : id of the block to add
+ def add_block
+ block = params[:block].to_s.underscore
+ if MyProjectsOverviewsController.available_blocks.keys.include? block
+ # remove if already present in a group
+ %w(top left right hidden).each {|f| overview.send(f).delete block }
+ # add it hidden
+ overview.hidden.unshift block
+ overview.save!
+ render partial: "block",
+ locals: { block_name: block }
+ elsif block == "custom_element"
+ overview.hidden.unshift overview.new_custom_element
+ overview.save!
+ render(partial: "block_textilizable",
+ locals: { user: user,
+ project: project,
+ block_title: l(:label_custom_element),
+ block_name: overview.hidden.first.first,
+ textile: overview.hidden.first.last })
+ else
+ render nothing: true
+ end
+ end
+
+ # Remove a block to user's page
+ # params[:block] : id of the block to remove
+ def remove_block
+ @block = param_to_block(params[:block])
+ %w(top left right hidden).each {|f| overview.send(f).delete @block }
+ overview.save!
+ end
+
+ # Change blocks order on user's page
+ # params[:group] : group to order (top, left or right)
+ # params[:list-(top|left|right)] : array of block ids of the group
+ def order_blocks
+ group = params[:group]
+ if group.is_a?(String)
+ group_items = (params["list-#{group}"] || []).collect {|x| param_to_block(x) }
+ unless group_items.size < overview.send(group).size
+ # We are adding or re-ordering, not removing
+ # Remove group blocks if they are presents in other groups
+ overview.update_attributes('top' => (overview.top - group_items),
+ 'left' => (overview.left - group_items),
+ 'right' => (overview.right - group_items),
+ 'hidden' => (overview.hidden - group_items))
+ overview.update_attribute(group, group_items)
+ end
+ end
+ end
+
+ def param_to_block(param)
+ block = param.to_s.underscore
+ unless MyProjectsOverviewsController.available_blocks.keys.include? block
+ block = overview.custom_elements.detect {|ary| ary.first == block}
+ end
+ block
+ end
+
+ def destroy_attachment
+ if user.allowed_to?(:edit_project, project)
+ begin
+ att = Attachment.find(params[:attachment_id].to_i)
+ overview.attachments.delete(att)
+ overview.save
+ rescue ActiveRecord::RecordNotFound
+ end
+ end
+
+ render partial: 'page_layout_attachments'
+ end
+
+ def show_all_members
+ respond_to do |format|
+ format.js {
+ render partial: "members",
+ locals: { users_by_role: users_by_role(0),
+ count_users_by_role: count_users_by_role }
+ }
+ end
+ end
+
+ helper_method :users_by_role,
+ :count_users_by_role,
+ :childprojects,
+ :recent_news,
+ :types,
+ :open_work_packages_by_type,
+ :total_work_packages_by_type,
+ :assigned_work_packages,
+ :total_hours,
+ :project,
+ :user,
+ :blocks,
+ :block_options,
+ :overview,
+ :attachments,
+ :render_block,
+ :object_callback
+
+ def childprojects
+ @childprojects ||= project.children.visible
+ end
+
+ def recent_news
+ @news ||= project
+ .news
+ .includes([:author, :project])
+ .order("#{News.table_name}.created_on DESC")
+ .limit(5)
+ end
+
+ def types
+ @types ||= project.rolled_up_types
+ end
+
+ def open_work_packages_by_type
+ @open_work_packages_by_tracker ||= work_packages_by_type
+ .where(statuses: { is_closed: false })
+ .count
+ end
+
+ def total_work_packages_by_type
+ @total_work_packages_by_tracker ||= work_packages_by_type.count
+ end
+
+ def work_packages_by_type
+ WorkPackage
+ .visible
+ .group(:type)
+ .includes([:project, :status, :type])
+ .where(subproject_condition)
+ end
+
+ def assigned_work_packages
+ @assigned_issues ||= WorkPackage
+ .visible
+ .open
+ .where(assigned_to: User.current.id)
+ .limit(10)
+ .includes([:status, :project, :type, :priority])
+ .order("#{IssuePriority.table_name}.position DESC,
+ #{WorkPackage.table_name}.updated_on DESC")
+ end
+
+ def users_by_role(limit = 100)
+ @users_by_role = Hash.new do |h, size|
+ h[size] = if size > 0
+ sql_string = all_roles.map do |r|
+ %Q{ (Select users.*, member_roles.role_id from users
+ JOIN members on users.id = members.user_id
+ JOIN member_roles on member_roles.member_id = members.id
+ WHERE members.project_id = #{project.id} AND member_roles.role_id = #{r.id}
+ LIMIT #{size} ) }
+ end.join(" UNION ALL ")
+
+ User.find_by_sql(sql_string).group_by(&:role_id).inject({}) do |hash, (role_id, users)|
+ hash[all_roles.detect{ |r| r.id == role_id.to_i }] = users.uniq {|user| user.id}
+ hash
+ end
+ else
+ project.users_by_role
+ end
+
+ end
+
+ @users_by_role[limit]
+ end
+
+ def count_users_by_role
+ @count_users_per_role ||= begin
+ sql_string = all_roles.map do |r|
+ %Q{ (Select COUNT(DISTINCT users.id) AS count, member_roles.role_id AS role_id from users
+ JOIN members on users.id = members.user_id
+ JOIN member_roles on member_roles.member_id = members.id
+ WHERE members.project_id = #{project.id} AND member_roles.role_id = #{r.id}
+ GROUP BY (member_roles.role_id)) }
+ end.join(" UNION ALL ")
+
+ role_count = {}
+
+ ActiveRecord::Base.connection.execute(sql_string).each do |entry|
+ if entry.is_a?(Hash)
+ # MySql
+ count = entry['count'].to_i
+ role_id = entry['role_id'].to_i
+ else
+ # Postgresql
+ count = entry.first.to_i
+ role_id = entry.last.to_i
+ end
+
+ role_count[all_roles.detect{ |r| r.id == role_id }] = count if count > 0
+ end
+
+ role_count
+ end
+ end
+
+ def all_roles
+ @all_roles = Role.all
+ end
+
+ def project
+ @project
+ end
+
+ def user
+ @user
+ end
+
+ def blocks
+ @blocks ||= {
+ 'top' => overview.top,
+ 'left' => overview.left,
+ 'right' => overview.right,
+ 'hidden' => overview.hidden
+ }
+ end
+
+ def block_options
+ @block_options = []
+ MyProjectsOverviewsController.available_blocks.each do |k, v|
+ @block_options << [l("my.blocks.#{v}", default: [v, v.to_s.humanize]), k.dasherize]
+ end
+ @block_options << [l(:label_custom_element), :custom_element]
+ end
+
+ def overview
+ @overview ||= MyProjectsOverview.find_or_create_by(project_id: project.id)
+ end
+
+ def attachments
+ @attachments = overview.attachments || []
+ end
+
+ private
+
+ def subproject_condition
+ @subproject_condition ||= project.project_condition(Setting.display_subprojects_work_packages?)
+ end
+
+ def find_user
+ @user = User.current
+ end
+
+ def default_breadcrumb
+ l(:label_overview)
+ end
+
+ def jump_to_project_menu_item
+ if params[:jump]
+ # try to redirect to the requested menu item
+ redirect_to_project_menu_item(project, params[:jump]) && return
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/app/helpers/my_projects_overviews_helper.rb b/vendored-plugins/openproject-my_project_page/app/helpers/my_projects_overviews_helper.rb
new file mode 100644
index 0000000000..05d48ce0a2
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/helpers/my_projects_overviews_helper.rb
@@ -0,0 +1,97 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module MyProjectsOverviewsHelper
+ include WorkPackagesFilterHelper
+
+ TOP = %w(top)
+ MIDDLE = %w(left right)
+ HIDDEN = %w(hidden)
+
+ def field_list
+ TOP + MIDDLE + HIDDEN
+ end
+
+ def visible_fields
+ TOP + MIDDLE
+ end
+
+ # TODO: potentially dangerous, is there a better way? (via define_method?)
+ def method_missing(name)
+ constant_name = name.to_s.gsub('_fields', '').upcase
+ if MyProjectsOverviewsHelper.const_defined? constant_name
+ return MyProjectsOverviewsHelper.const_get constant_name
+ end
+ raise NoMethodError.new("tried to call method #{name}, but was not found!")
+ end
+
+ def grid_field(name)
+ css_classes = %w(block-receiver list-position) + [name]
+ data = {
+ 'ajax-url' => ajax_url(name),
+ position: name
+ }
+ construct_blocks(name: name, css_classes: css_classes, data: data)
+ end
+
+ def rendered_field(name)
+ construct_blocks(name: name, css_classes: Array(name))
+ end
+
+ protected
+
+ def construct_blocks(opts = {})
+ name, css_classes, data = [:name, :css_classes, :data].map { |sym| opts.fetch sym, '' }
+ content_tag :div, id: "list-#{name}", class: css_classes, data: data do
+ ActiveSupport::SafeBuffer.new(blocks[name].map { |b| construct b }.join)
+ end
+ end
+
+ def block_available?(block)
+ controller.class.available_blocks.keys.include? block
+ end
+
+ def construct(block)
+ if block.is_a? Array
+ return render_textilized block
+ end
+ if block_available? block
+ return render_normal block
+ end
+ end
+
+ def ajax_url(name)
+ url_for controller: '/my_projects_overviews',
+ action: 'order_blocks',
+ group: name
+ end
+
+ def render_textilized(block)
+ render partial: 'block_textilizable', locals: {
+ block_name: block.first,
+ block_title: block[1],
+ textile: block.last
+ }
+ end
+
+ def render_normal(block)
+ render partial: 'block', locals: { block_name: block }
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/app/models/my_projects_overview.rb b/vendored-plugins/openproject-my_project_page/app/models/my_projects_overview.rb
new file mode 100644
index 0000000000..24b3c6e87b
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/models/my_projects_overview.rb
@@ -0,0 +1,83 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class MyProjectsOverview < ActiveRecord::Base
+
+ after_initialize :initialize_default_values
+
+ DEFAULTS = {
+ "left" => ["project_description", "project_details", "work_package_tracking"],
+ "right" => ["members", "news_latest"],
+ "top" => [],
+ "hidden" => [] }
+
+ def initialize_default_values()
+ # attributes() creates a copy every time it is called, so better not use it in a loop
+ # (this is also why we send the default-values instead of just setting it on attributes)
+ attr = attributes()
+
+ DEFAULTS.each_key do |attribute_name|
+ # mysql and postgres handle serialized arrays differently: This check initializes the defaults for both cases -
+ # this especially deals properly with the case where [] is written into the db and re-read ( which
+ # is not properly handled by a .blank?- check !!!)
+ self.send("#{attribute_name}=",DEFAULTS[attribute_name]) if attr[attribute_name].nil? || attr[attribute_name] ==""
+ end
+ end
+
+ serialize :top
+ serialize :left
+ serialize :right
+ serialize :hidden
+ belongs_to :project
+
+ validate :fields_are_arrays
+
+ acts_as_attachable delete_permission: :edit_project, view_permission: :view_project
+
+ def fields_are_arrays
+ Array === top && Array === left && Array === right && Array === hidden
+ end
+
+ def save_custom_element(name, title, new_content)
+ el = custom_elements.detect {|x| x.first == name}
+ return unless el
+ el[1] = title
+ el[2] = new_content
+ save
+ end
+
+ def new_custom_element
+ idx = custom_elements.any? ? custom_elements.sort.last.first.next : "a"
+ [idx, l(:label_custom_element), "h3. #{l(:info_custom_text)}"]
+ end
+
+ def elements
+ top + left + right + hidden
+ end
+
+ def custom_elements
+ elements.select {|x| x.respond_to? :to_ary }
+ end
+
+ def attachments_visible?(_user)
+ true
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block.html.erb
new file mode 100644
index 0000000000..cbf6d70973
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block.html.erb
@@ -0,0 +1,41 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block_textilizable.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block_textilizable.html.erb
new file mode 100644
index 0000000000..12a87ec511
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_block_textilizable.html.erb
@@ -0,0 +1,72 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_members.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_members.html.erb
new file mode 100644
index 0000000000..028f288a1d
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_members.html.erb
@@ -0,0 +1,28 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% users_by_role.keys.sort.each do |role| %>
+ <%=h role %>: <%= users_by_role[role].sort.collect{ |u| link_to_user u }.join("; ").html_safe %>
+ <% if users_by_role[role].count < count_users_by_role[role] %>
+ <%= l(:'project_page.x_more', :count => count_users_by_role[role] - users_by_role[role].count) %>
+ <% end %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_page_layout_attachments.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_page_layout_attachments.html.erb
new file mode 100644
index 0000000000..2c4c9b9e7b
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_page_layout_attachments.html.erb
@@ -0,0 +1,33 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% for attachment in attachments %>
+ <%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
+ <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
+ (<%= number_to_human_size attachment.filesize %>)
+ <%= link_to_remote icon_wrapper('icon-context icon-delete', l(:button_delete)),
+ :url => { :action => 'destroy_attachment', :attachment_id => attachment.id, :id => project.id },
+ :confirm => l(:text_are_you_sure),
+ :class => 'delete no-decoration-on-hover',
+ :title => l(:button_delete),
+ :update => 'page_layout_attachments' %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_sidebar.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_sidebar.html.erb
new file mode 100644
index 0000000000..014a773f67
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_sidebar.html.erb
@@ -0,0 +1,54 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<%=l(:label_my_account)%>
+
+<%=l(:field_login)%>: <%= link_to_user(@user, :format => :username) %>
+<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %>
+
+
+<%= l(:label_feeds_access_key) %>
+
+
+<% if @user.rss_token %>
+<%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %>
+<% else %>
+<%= l(:label_missing_feeds_access_key) %>
+<% end %>
+(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)
+
+
+<% if Setting.rest_api_enabled? %>
+<%= l(:label_api_access_key) %>
+
+ <%= link_to_function(l(:button_show), "$('api-access-key').toggle();")%>
+
<%= h(@user.api_key) %>
+
+<%= javascript_tag("$('api-access-key').hide();") %>
+
+<% if @user.api_token %>
+<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
+<% else %>
+<%= l(:label_missing_api_access_key) %>
+<% end %>
+(<%= link_to l(:button_reset), {:action => 'reset_api_key'}, :method => :post %>)
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_textilizable.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_textilizable.html.erb
new file mode 100644
index 0000000000..9e65dc8e6e
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/_textilizable.html.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+
+
+<% if defined? block_name %>
+ <%= content_for block_name %>
+<% end %>
+
+<% if defined? block_name %>
+
+ <%= textilizable(textile, :object => overview) %>
+
+<% else %>
+ <%= textilizable(textile, :object => overview) %>
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_calendar.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_calendar.html.erb
new file mode 100644
index 0000000000..d518fe8039
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_calendar.html.erb
@@ -0,0 +1,35 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% calendar = Redmine::Helpers::Calendar.new(Date.today, current_language, :week)
+ calendar.events = WorkPackage.visible.where("#{WorkPackage.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')}) AND ((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", calendar.startdt, calendar.enddt, calendar.startdt, calendar.enddt)
+ .includes(:project, :type, :priority, :assigned_to) unless @user.projects.empty? %>
+
+<%= render :partial => 'common/calendar', :locals => {:calendar => calendar } %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_members.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_members.html.erb
new file mode 100644
index 0000000000..85e9041b89
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_members.html.erb
@@ -0,0 +1,47 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% if @project.members.count > 0 && users_by_role(50).any? %>
+
+
+ <%= render :partial => "members",
+ :locals => { :users_by_role => users_by_role(50),
+ :count_users_by_role => count_users_by_role } %>
+
+
+ <% if users_by_role(50).any?{ |role, users| users.count < count_users_by_role[role] } %>
+ <%= link_to(l(:'project_page.all'), { :controller => 'my_projects_overviews',
+ :action => 'show_all_members',
+ :id => project.id },
+ :class => "all button -highlight" ) %>
+ <% end %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_news_latest.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_news_latest.html.erb
new file mode 100644
index 0000000000..c3123b613e
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_news_latest.html.erb
@@ -0,0 +1,42 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% if recent_news.any? && authorize_for('news', 'index') %>
+
+ <%= render :partial => 'news/news', :collection => recent_news %>
+
+ <%= link_to l(:label_news_view_all),
+ {:controller => 'news',
+ :action => 'index',
+ :project_id => project},
+ :class => 'button -highlight' %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_description.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_description.html.erb
new file mode 100644
index 0000000000..ba452733e0
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_description.html.erb
@@ -0,0 +1,32 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+
+ <%= textilizable @project.description %>
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_details.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_details.html.erb
new file mode 100644
index 0000000000..1ae120931a
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_project_details.html.erb
@@ -0,0 +1,42 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+
+
+ <% if childprojects.any? %>
+ <%=l(:label_subproject_plural)%>:
+ <%= childprojects.collect{|p| link_to(h(p), project_url(p))}.join(", ").html_safe %>
+ <% end %>
+ <% project.visible_custom_field_values.each do |custom_value| %>
+ <% if !custom_value.value.blank? %>
+ <%= h(custom_value.custom_field.name) %>: <%=h show_value(custom_value) %>
+ <% end %>
+ <% end %>
+
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_spent_time.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_spent_time.html.erb
new file mode 100644
index 0000000000..6abaf69cd1
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_spent_time.html.erb
@@ -0,0 +1,134 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<%
+ entries = TimeEntry
+ .includes([:activity, :project, {:work_package => [:type, :status]}])
+ .where(["#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", @user.id, Date.today - 6, Date.today]).references(:projects)
+ .order("#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Type.table_name}.position ASC, #{WorkPackage.table_name}.id ASC")
+ entries_by_day = entries.group_by(&:spent_on)
+%>
+
+
+
<%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(:hours).to_f) %>
+
+
+<% if entries.any? %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% entries_by_day.keys.sort.reverse.each do |day| %>
+
+ <%= day == Date.today ? l(:label_today).titleize : format_date(day) %>
+
+
+ <%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %>
+
+
+ <% entries_by_day[day].each do |entry| -%>
+
+ <%=h entry.activity %>
+ <%=h entry.project %> <%= ' - '.html_safe + link_to_work_package(entry.work_package, :truncate => 50) if entry.work_package%>
+
+ <%= html_hours("%.2f" % entry.hours) %>
+
+ <% if entry.editable_by?(@user) -%>
+ <%= link_to icon_wrapper('icon-context icon-edit', t(:button_edit)),
+ {:controller => '/timelog', :action => 'edit', :id => entry},
+ :alt => l(:button_edit),
+ :class => 'no-decoration-on-hover',
+ :title => l(:button_edit) %>
+ <%= link_to icon_wrapper('icon-context icon-delete', t(:button_delete)),
+ {:controller => '/timelog', :action => 'destroy', :id => entry},
+ :confirm => l(:text_are_you_sure),
+ :method => :delete,
+ :class => 'no-decoration-on-hover',
+ :alt => l(:button_delete),
+ :title => l(:button_delete) %>
+ <% end -%>
+
+
+ <% end -%>
+ <% end -%>
+
+
+
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_subprojects.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_subprojects.html.erb
new file mode 100644
index 0000000000..6c4b9d4bb2
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_subprojects.html.erb
@@ -0,0 +1,36 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% if childprojects.any? %>
+
+
+ <%= childprojects.collect{ |p| link_to(h(p), project_url(p)) }.join(", ").html_safe %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_package_tracking.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_package_tracking.html.erb
new file mode 100644
index 0000000000..b4b2c7a6c4
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_package_tracking.html.erb
@@ -0,0 +1,51 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% if User.current.allowed_to?(:view_work_packages, project) %>
+
+
+ <% for type in types %>
+ <%= link_to h(type.name), project_property_path(project, "type_id", type.id) %>:
+ <%= l(:label_x_open_work_packages_abbr_on_total, :count => open_work_packages_by_type[type].to_i,
+ :total => total_work_packages_by_type[type].to_i) %>
+
+ <% end %>
+
+
+ <%= link_to l(:label_work_package_view_all), {:controller => '/work_packages', :action => 'index', :project_id => project}, :class => 'button -highlight' %>
+ <% if User.current.allowed_to?(:view_calendar, project, :global => true) %>
+ <%= link_to(l(:label_calendar), {:controller => '/work_packages/calendars', :action => 'index', :project_id => project}, :class => 'button -highlight') %>
+ <% end %>
+ <% if User.current.allowed_to?(:view_gantt, project, :global => true) %>
+ <%= link_to(l(:label_gantt), {:controller => '/gantts', :action => 'show', :project_id => project}, :class => 'button -highlight') %>
+ <% end %>
+
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_assigned_to_me.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_assigned_to_me.html.erb
new file mode 100644
index 0000000000..f972c6572b
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_assigned_to_me.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% assigned_work_packages = WorkPackage.visible.open
+ .where(assigned_to_id: User.current.id, project_id: @project.id)
+ .limit(10)
+ .includes([:status, :project, :type, :priority])
+ .order("#{IssuePriority.table_name}.position DESC, #{WorkPackage.table_name}.updated_at DESC") %>
+<%= render partial: 'work_packages/list_simple', :locals => { :work_packages => assigned_work_packages, list_for: :assigned } %>
+<% if assigned_work_packages.length > 0 %>
+
+ <%= link_to l(:label_work_package_view_all_assigned_to_me),
+ project_work_packages_assigned_to_me_path(@project),
+ :class => 'button -highlight' %>
+
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom,
+ project_work_packages_assigned_to_me_path(@project, {:format => 'atom', :key => User.current.rss_key}),
+ {:title => l(:label_work_packages_assigned_to_me)}) %>
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_reported_by_me.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_reported_by_me.html.erb
new file mode 100644
index 0000000000..e4f7ea69a7
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_reported_by_me.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% reported_work_packages = WorkPackage.visible
+ .where(author_id: User.current.id, project_id: @project.id)
+ .limit(10)
+ .includes([:status, :project, :type])
+ .order("#{WorkPackage.table_name}.updated_at DESC") %>
+<%= render :partial => 'work_packages/list_simple', :locals => { :work_packages => reported_work_packages, list_for: :reported } %>
+<% if reported_work_packages.length > 0 %>
+
+ <%= link_to l(:label_work_package_view_all_reported_by_me),
+ project_work_packages_reported_by_me_path(@project),
+ :class => 'button -highlight' %>
+
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom,
+ project_work_packages_reported_by_me_path(@project, {:format => 'atom', :key => User.current.rss_key}),
+ {:title => l(:label_work_packages_reported_by_me)}) %>
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_responsible_for.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_responsible_for.html.erb
new file mode 100644
index 0000000000..d3d0c7a06e
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_responsible_for.html.erb
@@ -0,0 +1,49 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% responsible_work_packages = WorkPackage.visible.open
+ .where(responsible_id: User.current.id, project_id: @project.id)
+ .limit(10)
+ .includes([:status, :project, :type, :priority])
+ .order("#{IssuePriority.table_name}.position DESC, #{WorkPackage.table_name}.updated_at DESC") %>
+<%= render :partial => 'work_packages/list_simple', :locals => { :work_packages => responsible_work_packages, list_for: :responsible } %>
+<% if responsible_work_packages.length > 0 %>
+
+ <%= link_to l(:label_work_package_view_all_responsible_for),
+ project_work_packages_responsible_for_path(@project),
+ :class => 'button -highlight' %>
+
+<% end %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom,
+ project_work_packages_responsible_for_path(@project, {:format => 'atom', :key => User.current.rss_key}),
+ {:title => l(:label_work_packages_responsible_for)}) %>
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_watched.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_watched.html.erb
new file mode 100644
index 0000000000..466665b95d
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/blocks/_work_packages_watched.html.erb
@@ -0,0 +1,40 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% if defined? block_name_id %>
+ <%= content_for block_name_id %>
+<% end %>
+
+
+
+<% watched_work_packages = WorkPackage.where(project_id: @project.id).visible.on_active_project.watched_by(@user.id).recently_updated.with_limit(10) %>
+
+<%= render :partial => 'work_packages/list_simple', :locals => { :work_packages => watched_work_packages, list_for: :watched } %>
+<% if watched_work_packages.length > 0 %>
+
+ <%= link_to l(:label_work_package_view_all_watched),
+ project_work_packages_watched_path(@project),
+ :class => 'button -highlight' %>
+
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/index.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/index.html.erb
new file mode 100644
index 0000000000..e2f2ab4e8e
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/index.html.erb
@@ -0,0 +1,50 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= javascript_include_tag "my_project_page/my_project_page" %>
+ <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
+ <%= stylesheet_link_tag "my_project_page/my_projects_overview", :media => 'all' %>
+<% end %>
+
+<% html_title(l(:label_overview)) -%>
+
+<%= toolbar title: l(:label_overview) do %>
+ <% if User.current.allowed_to?(:edit_project, project) %>
+
+ <%= link_to my_projects_overview_path(project), class: 'button', accesskey: accesskey(:edit) do %>
+
+ <%= l(:label_personalize_page) %>
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/order_blocks.js.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/order_blocks.js.erb
new file mode 100644
index 0000000000..584a30fdb9
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/order_blocks.js.erb
@@ -0,0 +1,2 @@
+// rebind some of the links if the block is textilizable
+myPage.updateBlockLinks();
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/page_layout.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/page_layout.html.erb
new file mode 100644
index 0000000000..1608510a2a
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/page_layout.html.erb
@@ -0,0 +1,69 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+<% html_title(l(:label_overview)) -%>
+
+<% content_for :header_tags do %>
+ <%= javascript_include_tag "my_project_page/my_project_page" %>
+ <%= stylesheet_link_tag "my_project_page/my_projects_overview", :media => 'all' %>
+ <% heads_for_wiki_formatter %>
+<% end %>
+
+<%= toolbar title: l(:label_overview) do %>
+
+ <%= styled_form_tag({:action => "add_block"}, :id => "block-form") do %>
+ <%= styled_select_tag 'block',
+ ("--#{t(:button_add)}-- " + options_for_select(block_options)).html_safe,
+ :id => "block-select",
+ class: 'form--select'
+ %>
+ <% end %>
+
+ <%= link_to l(:button_back), {:action => 'index'}, class: 'button' %>
+<% end %>
+
+
+<%=l(:label_visible_elements) %>
+
+
+<%=l(:label_hidden_elements) %>
+
+ <% hidden_fields.each do |f| %>
+ <%= grid_field f %>
+ <% end %>
+
+
+<%= l(:label_file_plural) %>
+
+ <%= render(:partial => "page_layout_attachments") %>
+
+
+
+
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/password.html.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/password.html.erb
new file mode 100644
index 0000000000..143e46c8fa
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/password.html.erb
@@ -0,0 +1,44 @@
+<%#-- copyright
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_my_account), l(:button_change_password) %>
+<%= toolbar title: l(:button_change_password) %>
+
+<%= error_messages_for 'user' %>
+
+<% form_tag({}, :class => "tabular") do %>
+
+
<%=l(:field_password)%> *
+<%= password_field_tag 'password', nil, :size => 25 %>
+
+
<%=l(:field_new_password)%> *
+<%= password_field_tag 'new_password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>
+
+
<%=l(:field_password_confirmation)%> *
+<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>
+
+<%= styled_button_tag l(:button_apply), class: '-highlight -with-icon icon-checkmark' %>
+<% end %>
+
+<% content_for :sidebar do %>
+<%= render :partial => 'sidebar' %>
+<% end %>
diff --git a/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/remove_block.js.erb b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/remove_block.js.erb
new file mode 100644
index 0000000000..69953cc0f3
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/app/views/my_projects_overviews/remove_block.js.erb
@@ -0,0 +1,7 @@
+(function($) {
+ var block = $('#block_' + '<%= @block.is_a?(Array) ? @block.first : @block %>');
+ block.fadeOut('fast', function() {
+ block.remove();
+ myPage.updateSelect();
+ });
+}(jQuery))
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/da.yml b/vendored-plugins/openproject-my_project_page/config/locales/da.yml
new file mode 100644
index 0000000000..bddb45bad4
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/da.yml
@@ -0,0 +1,18 @@
+da:
+ label_hidden_elements: Gemte elementer
+ label_visible_elements: Synlige elementer
+ label_project_description: Projektbeskrivelse
+ label_project_details: Projektdetaljer
+ label_custom_element: Tilføj appetitvækker...
+ info_custom_text: Tilpasset tekst
+ label_confirm_delete: Vil du virkelig slette dette element?
+ label_work_packages_reported_by_me: Arbejdspakker der rapporterer til mig
+ label_work_packages_responsible_for: Arbejdspakker jeg er ansvarlig for
+ label_work_packages_assigned_to_me: Arbejdspakker tilknyttet mig
+ label_work_packages_watched: Overvågede arbejdspakker
+ label_work_package_tracking: Sporing af arbejdspakke
+ label_members: Medlemmer
+ label_subprojects: Underprojekter
+ project_page:
+ all: Vis alt
+ x_more: '...(%{count} mere)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/de.yml b/vendored-plugins/openproject-my_project_page/config/locales/de.yml
new file mode 100644
index 0000000000..21daa622db
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/de.yml
@@ -0,0 +1,18 @@
+de:
+ label_hidden_elements: Versteckte Elemente
+ label_visible_elements: Sichtbare Elemente
+ label_project_description: Projektbeschreibung
+ label_project_details: Projektdetails
+ label_custom_element: Teaser hinzufügen ...
+ info_custom_text: Benutzerdefinierter Text
+ label_confirm_delete: Dieses Element wirklich löschen?
+ label_work_packages_reported_by_me: Von mir gemeldete Arbeitspakete
+ label_work_packages_responsible_for: Von mir verantwortete Arbeitspakete
+ label_work_packages_assigned_to_me: Mir zugewiesene Tickets
+ label_work_packages_watched: Beobachtete Arbeitspakete
+ label_work_package_tracking: Arbeitspakete-Verfolgung
+ label_members: Mitglieder
+ label_subprojects: Unterprojekte
+ project_page:
+ all: Alle anzeigen
+ x_more: '...(%{count} weitere)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/en.yml b/vendored-plugins/openproject-my_project_page/config/locales/en.yml
new file mode 100644
index 0000000000..ad2e48c616
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/en.yml
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+en:
+ label_hidden_elements: Hidden elements
+ label_visible_elements: Visible elements
+ label_project_description: Project description
+ label_project_details: Project details
+ label_custom_element: Add teaser ...
+ info_custom_text: Custom text
+ label_confirm_delete: Really delete this element?
+ label_work_packages_reported_by_me: Work packages reported by me
+ label_work_packages_responsible_for: Work packages I am responsible for
+ label_work_packages_assigned_to_me: Work packages assigned to me
+ label_work_packages_watched: Watched work packages
+ label_work_package_tracking: Work package tracking
+ label_members: Members
+ label_subprojects: Subprojects
+
+ project_page:
+ all: "Show all"
+ x_more: "...(%{count} more)"
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/es-ES.yml b/vendored-plugins/openproject-my_project_page/config/locales/es-ES.yml
new file mode 100644
index 0000000000..3e3e5f2a45
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/es-ES.yml
@@ -0,0 +1,18 @@
+es:
+ label_hidden_elements: Elementos ocultos
+ label_visible_elements: Elementos visibles
+ label_project_description: Descripción del proyecto
+ label_project_details: Detalles del proyecto
+ label_custom_element: Añadir sumario ...
+ info_custom_text: Texto personalizado
+ label_confirm_delete: ¿Realmente desea eliminar este elemento?
+ label_work_packages_reported_by_me: Paquetes de trabajo reportados por mí
+ label_work_packages_responsible_for: Paquetes de trabajo de los que soy responsable
+ label_work_packages_assigned_to_me: Paquetes de trabajo asignados a mí
+ label_work_packages_watched: Paquetes de trabajo vistos
+ label_work_package_tracking: Seguimiento de paquetes de trabajo
+ label_members: Miembros
+ label_subprojects: Subproyectos
+ project_page:
+ all: Mostrar todo
+ x_more: '... (%{count} más)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/fr.yml b/vendored-plugins/openproject-my_project_page/config/locales/fr.yml
new file mode 100644
index 0000000000..0e3616230d
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/fr.yml
@@ -0,0 +1,20 @@
+fr:
+ label_hidden_elements: Éléments masqués
+ label_visible_elements: Éléments visibles
+ label_project_description: Description du projet
+ label_project_details: Détails du projet
+ label_custom_element: Ajouter une accroche…
+ info_custom_text: Texte personnalisé
+ label_confirm_delete: Êtes-vous sûr de vouloir supprimer cet élément ?
+ label_work_packages_reported_by_me: "Lots de travaux que j'ai consignés"
+ label_work_packages_responsible_for: Lots de travaux dont je suis responsable
+ label_work_packages_assigned_to_me: Lots de travaux qui me sont assignés
+ label_work_packages_watched: Lots de travaux surveillés
+ label_work_package_tracking: Suivi des lots de travaux
+ label_members: Membres
+ label_subprojects: Sous-projets
+ project_page:
+ all: Afficher tout
+ x_more: |
+ …(%{count} de plus)
+
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/it.yml b/vendored-plugins/openproject-my_project_page/config/locales/it.yml
new file mode 100644
index 0000000000..3a0429b300
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/it.yml
@@ -0,0 +1,18 @@
+it:
+ label_hidden_elements: Elementi nascosti
+ label_visible_elements: Elementi visibili
+ label_project_description: Descrizione del progetto
+ label_project_details: Dettagli del progetto
+ label_custom_element: Aggiungere teaser...
+ info_custom_text: Testo personalizzato
+ label_confirm_delete: Sicuro di voler eliminare questo elemento?
+ label_work_packages_reported_by_me: Pacchetti di lavoro segnalati da me
+ label_work_packages_responsible_for: Pacchetti di lavoro di cui sono responsabile
+ label_work_packages_assigned_to_me: Pacchetti di lavoro assegnati a me
+ label_work_packages_watched: Pacchetti di lavoro osservati
+ label_work_package_tracking: Monitoraggio pacchetti di lavoro
+ label_members: Membri
+ label_subprojects: Sottoprogetti
+ project_page:
+ all: Mostra tutti
+ x_more: '... (più%{count})'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/lv.yml b/vendored-plugins/openproject-my_project_page/config/locales/lv.yml
new file mode 100644
index 0000000000..15ecbfa458
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/lv.yml
@@ -0,0 +1,18 @@
+lv:
+ label_hidden_elements: Paslēptie elementi
+ label_visible_elements: Redzamie elementi
+ label_project_description: Projekta apraksts
+ label_project_details: Detalizēta informācija par projektu
+ label_custom_element: 'Pievienot "teaser"...'
+ info_custom_text: Pielāgots teksts
+ label_confirm_delete: Vai tiešām dzēst šo elementu?
+ label_work_packages_reported_by_me: Mani izveidotie pieteikumi
+ label_work_packages_responsible_for: Pieteikumi, kuros es esmu atbildīgais
+ label_work_packages_assigned_to_me: Man piešķirtie pieteikumi
+ label_work_packages_watched: Pieteikumi, kuriem tiek sekots
+ label_work_package_tracking: Pieteikuma sekošana
+ label_members: Dalībnieki
+ label_subprojects: Apakšprojekti
+ project_page:
+ all: Rādīt visus
+ x_more: '... (vēl%{count})'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/pl.yml b/vendored-plugins/openproject-my_project_page/config/locales/pl.yml
new file mode 100644
index 0000000000..8e20530a68
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/pl.yml
@@ -0,0 +1,18 @@
+pl:
+ label_hidden_elements: Ukryte elementy
+ label_visible_elements: Elementy widoczne
+ label_project_description: Opis projektu
+ label_project_details: Szczegóły Projektu
+ label_custom_element: Dodać zapowiedź...
+ info_custom_text: Tekst niestandardowy/użytkownika
+ label_confirm_delete: Naprawdę usunąć ten element?
+ label_work_packages_reported_by_me: Zadania zgłoszone przeze mnie
+ label_work_packages_responsible_for: Zadania, za które jestem odpowiedzialny
+ label_work_packages_assigned_to_me: Zadania przypisane do mnie
+ label_work_packages_watched: Obserwowane zadania
+ label_work_package_tracking: Śledzenie zadania
+ label_members: Członkowie
+ label_subprojects: Podprojekty
+ project_page:
+ all: Pokaż wszystkie
+ x_more: '... (%{count} więcej)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/pt-BR.yml b/vendored-plugins/openproject-my_project_page/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..e2fcef7a83
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/pt-BR.yml
@@ -0,0 +1,18 @@
+pt-BR:
+ label_hidden_elements: Elementos ocultos
+ label_visible_elements: Elementos visíveis
+ label_project_description: Descrição do projeto
+ label_project_details: Detalhes do projeto
+ label_custom_element: Adicionar chamada...
+ info_custom_text: Texto personalizado
+ label_confirm_delete: Confirma a exclusão deste elemento?
+ label_work_packages_reported_by_me: Pacotes de trabalho criados por mim
+ label_work_packages_responsible_for: Pacotes de trabalho sob minha responsabilidade
+ label_work_packages_assigned_to_me: Pacotes de trabalho atribuídos a mim
+ label_work_packages_watched: Pacotes de trabalho observados
+ label_work_package_tracking: Acompanhamento de pacote de trabalho
+ label_members: Membros
+ label_subprojects: Subprojetos
+ project_page:
+ all: Mostrar todos
+ x_more: '... (%{count} mais)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/ru.yml b/vendored-plugins/openproject-my_project_page/config/locales/ru.yml
new file mode 100644
index 0000000000..0ae2033b12
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/ru.yml
@@ -0,0 +1,18 @@
+ru:
+ label_hidden_elements: Скрытые элементы
+ label_visible_elements: Видимые элементы
+ label_project_description: Описание проекта
+ label_project_details: Детали проекта
+ label_custom_element: Добавить тизер...
+ info_custom_text: Пользовательский текст
+ label_confirm_delete: Действительно удалить этот элемент?
+ label_work_packages_reported_by_me: Работа пакеты, за которые я отчитался
+ label_work_packages_responsible_for: Пакеты работ, за которые я несу ответственность
+ label_work_packages_assigned_to_me: Пакеты работ, предназначенные мне
+ label_work_packages_watched: Наблюдаемые пакеты работ
+ label_work_package_tracking: Отслеживание пакета работ
+ label_members: Участники
+ label_subprojects: Подпроекты
+ project_page:
+ all: Показывать всё
+ x_more: '... (более %{count})'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/sk.yml b/vendored-plugins/openproject-my_project_page/config/locales/sk.yml
new file mode 100644
index 0000000000..05c5551997
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/sk.yml
@@ -0,0 +1,18 @@
+sk:
+ label_hidden_elements: Skryté prvky
+ label_visible_elements: Viditeľné prvky
+ label_project_description: Popis projektu
+ label_project_details: Detailne informácie o projekte
+ label_custom_element: 'Pridať "teaser" ...'
+ info_custom_text: Vlastný text
+ label_confirm_delete: Naozaj chcete odstrániť tento prvok?
+ label_work_packages_reported_by_me: Pracovné balíčky, ktoré som reportoval
+ label_work_packages_responsible_for: Pracovné balíčky, za ktoré som zodpovedný(a)
+ label_work_packages_assigned_to_me: Mne priradené pracovné balíčky
+ label_work_packages_watched: Pozorované pracovné balíčky
+ label_work_package_tracking: Sledovanie pracovného balíčka
+ label_members: Členovia
+ label_subprojects: Podprojekty
+ project_page:
+ all: Zobraziť všetko
+ x_more: '... (ďalších %{count})'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/sv-SE.yml b/vendored-plugins/openproject-my_project_page/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..766a100572
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/sv-SE.yml
@@ -0,0 +1,18 @@
+sv:
+ label_hidden_elements: Dolda element
+ label_visible_elements: Synliga element
+ label_project_description: Projektbeskrivning
+ label_project_details: Projektinformation
+ label_custom_element: Lägg till teaser...
+ info_custom_text: Egen text
+ label_confirm_delete: Vill du verkligen ta bort detta element?
+ label_work_packages_reported_by_me: Arbetspaket rapporterade av mig
+ label_work_packages_responsible_for: Arbetspaket jag är ansvarig för
+ label_work_packages_assigned_to_me: Arbetspaket som tilldelats till mig
+ label_work_packages_watched: Bevakade arbetspaket
+ label_work_package_tracking: Arbetspaketsspårning
+ label_members: Medlemmar
+ label_subprojects: Delprojekt
+ project_page:
+ all: Visa alla
+ x_more: '... (%{count} mer)'
diff --git a/vendored-plugins/openproject-my_project_page/config/locales/tr.yml b/vendored-plugins/openproject-my_project_page/config/locales/tr.yml
new file mode 100644
index 0000000000..f9b408f843
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/locales/tr.yml
@@ -0,0 +1,18 @@
+tr:
+ label_hidden_elements: Gizli öğeler
+ label_visible_elements: Görünür öğeler
+ label_project_description: Proje tanımı
+ label_project_details: Proje detayları
+ label_custom_element: Tanıtım ekle ...
+ info_custom_text: Özel metin
+ label_confirm_delete: Bu öğeyi gerçekten silmek istiyor musunuz?
+ label_work_packages_reported_by_me: Work packages reported by me
+ label_work_packages_responsible_for: Benim sorumlu olduğum iş paketleri
+ label_work_packages_assigned_to_me: Bana atanan iş paketleri
+ label_work_packages_watched: Watched work packages
+ label_work_package_tracking: Work package tracking
+ label_members: Üyeler
+ label_subprojects: Alt projeler
+ project_page:
+ all: Tümünü görüntüle
+ x_more: '... (%{count} daha fazla)'
diff --git a/vendored-plugins/openproject-my_project_page/config/routes.rb b/vendored-plugins/openproject-my_project_page/config/routes.rb
new file mode 100644
index 0000000000..01a7eb3b42
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/config/routes.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+# re-write
+OpenProject::Application.routes.draw do
+ # replace the standard overview-page with the my-project-page
+ # careful: do not over-match the reserved path like /projects/new or /projects/level_list, see http://rubular.com/r/1uoiXyApCB
+ get 'projects/:id', to: "my_projects_overviews#index" ,
+ constraints: { id: Regexp.new("(?!(#{Project::RESERVED_IDENTIFIERS.join('|')})$)(\\w|-)+") }
+
+
+
+
+ get 'my_projects_overview/:id/page_layout', to: "my_projects_overviews#page_layout",
+ as: :my_projects_overview
+ post 'my_projects_overview/:id/page_layout/order_blocks', to: "my_projects_overviews#order_blocks"
+ post 'my_projects_overview/:id/page_layout/remove_block', to: "my_projects_overviews#remove_block"
+ post 'my_projects_overview/:id/page_layout/add_block', to: "my_projects_overviews#add_block"
+ put 'my_projects_overview/:id/page_layout/update_custom_element', to: "my_projects_overviews#update_custom_element"
+ post 'my_projects_overview/:id/page_layout/destroy_attachment', to: "my_projects_overviews#destroy_attachment"
+ get 'my_projects_overview/:id/page_layout/show_all_members', to: "my_projects_overviews#show_all_members"
+end
diff --git a/vendored-plugins/openproject-my_project_page/db/migrate/20120605121861_aggregated_my_project_page_migrations.rb b/vendored-plugins/openproject-my_project_page/db/migrate/20120605121861_aggregated_my_project_page_migrations.rb
new file mode 100644
index 0000000000..17dab79186
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/db/migrate/20120605121861_aggregated_my_project_page_migrations.rb
@@ -0,0 +1,59 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+require Rails.root.join("db","migrate","migration_utils","migration_squasher").to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedMyProjectPageMigrations < ActiveRecord::Migration
+
+ MIGRATION_FILES = <<-MIGRATIONS
+ 20110804151010_add_projects_overviews.rb
+ 20111202150017_change_serialized_columns_from_string_to_text.rb
+ 20120605121847_create_default_my_projects_page.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = "chiliproject_my_project_page"
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table :my_projects_overviews do |t|
+ t.integer "project_id", default: 0, null: false
+ t.text "left", null: false
+ t.text "right", null: false
+ t.text "top", null: false
+ t.text "hidden", null: false
+ t.datetime "created_on", null: false
+ end
+
+ # creates a default my project page config for each project
+ # that pretty much mirrors the contents of the static page
+ # if there is already a my project page then don't create a second one
+ Project.all.each do |project|
+ unless MyProjectsOverview.exists? project_id: project.id
+ MyProjectsOverview.create project: project
+ end
+ end
+ end
+ end
+
+ def down
+ drop_table :my_projects_overviews
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/db/migrate/20130903172842_my_project_page_migrate_serialized_yaml.rb b/vendored-plugins/openproject-my_project_page/db/migrate/20130903172842_my_project_page_migrate_serialized_yaml.rb
new file mode 100644
index 0000000000..9b4abd8b60
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/db/migrate/20130903172842_my_project_page_migrate_serialized_yaml.rb
@@ -0,0 +1,35 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require Rails.root.join("db","migrate","migration_utils","legacy_yamler").to_s
+
+class MyProjectPageMigrateSerializedYaml < ActiveRecord::Migration
+ include Migration::LegacyYamler
+
+ def up
+ ['top', 'left', 'right', 'hidden'].each do |column|
+ migrate_to_psych('my_projects_overviews', column)
+ end
+ end
+
+ def down
+ puts 'YAML data serialized with Psych is still compatible with Syck. Skipping migration.'
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/db/migrate/20130904181242_rename_blocks_keys.rb b/vendored-plugins/openproject-my_project_page/db/migrate/20130904181242_rename_blocks_keys.rb
new file mode 100644
index 0000000000..d1d4c96cbc
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/db/migrate/20130904181242_rename_blocks_keys.rb
@@ -0,0 +1,54 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class RenameBlocksKeys < ActiveRecord::Migration
+ REPLACED = {
+ "issuesassignedtome" => "work_packages_assigned_to_me",
+ "issuesreportedbyme" => "work_packages_reported_by_me",
+ "issuetracking" => "work_package_tracking",
+ "issueswatched" => "work_packages_watched",
+ "news" => "news_latest",
+ "timelog" => "spent_time",
+ "projectdetails" => "project_details",
+ "projectdescription" => "project_description"
+ }
+
+ def self.up
+ migrate(REPLACED)
+ end
+
+ def self.down
+ migrate(REPLACED.invert)
+ end
+
+ def self.migrate(replacer)
+ MyProjectsOverview.all.each do |my_project_overview|
+ ['top', 'left', 'right', 'hidden'].each do |attribute|
+ old = my_project_overview.send(attribute)
+ my_project_overview.send(attribute+'=',replace(old,replacer))
+ end
+ my_project_overview.save!
+ end
+ end
+
+ def self.replace(array, replacer)
+ array.map { |element| replacer[element] ? replacer[element] : element }
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT.md b/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..ddd4d13376
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject My Project Page Plugin
+
+This plugin provides a customizable view of the Project-Overview-Page.
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..ecdd15f36c
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/doc/COPYRIGHT_short.md
@@ -0,0 +1,17 @@
+OpenProject My Project Page Plugin
+
+Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
diff --git a/vendored-plugins/openproject-my_project_page/doc/GPL.txt b/vendored-plugins/openproject-my_project_page/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-my_project_page/features/blocks.feature b/vendored-plugins/openproject-my_project_page/features/blocks.feature
new file mode 100644
index 0000000000..a5b2bb99c0
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/blocks.feature
@@ -0,0 +1,106 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Behavior of specific blocks (news, issues - this is currently not complete!!)
+ Background:
+ Given there is 1 project with the following:
+ | name | tested_project |
+ And the project "tested_project" has the following types:
+ | name | position |
+ | Bug | 1 |
+ And there is 1 project with the following:
+ | name | other_project |
+ And the project "other_project" has the following types:
+ | name | position |
+ | Bug | 1 |
+ And there is 1 user with the following:
+ | login | bob |
+ And there is 1 user with the following:
+ | login | mary |
+ And there is a role "member"
+ And the role "member" may have the following rights:
+ | view_work_packages |
+ | create_work_packages |
+ And the user "bob" is a "member" in the project "tested_project"
+ And the user "bob" is a "member" in the project "other_project"
+ And I am logged in as "bob"
+
+
+
+ Scenario: In the news Section, I should only see news for the selected project
+ And project "tested_project" uses the following modules:
+ | news |
+ And the following widgets are selected for the overview page of the "tested_project" project:
+ #TODO mapping from the human-name back to it's widget-name??!
+ | top | news_latest |
+ Given there is a news "test-headline" for project "tested_project"
+ And there is a news "NO-SHOW" for project "other_project"
+ And I am on the homepage for the project "tested_project"
+ Then I should see the widget "news_latest"
+ And I should see the news-headline "test-headline"
+ And I should not see the news-headline "NO-SHOW"
+
+ Scenario: In the 'Work packages reported by me'-Section, I should only see work packages for the selected project
+ And there are the following issues with attributes:
+ | subject | project | author |
+ | Test-Issue | tested_project | bob |
+ | NO-SHOW | other_project | bob |
+ And the following widgets are selected for the overview page of the "tested_project" project:
+ | top | work_packages_reported_by_me |
+ And I am on the homepage for the project "tested_project"
+ Then I should see the widget "work_packages_reported_by_me"
+ And I should see the work-package-subject "Test-Issue" in the 'Work packages reported by me'-section
+ And I should not see the work-package-subject "NO-SHOW" in the 'Work packages reported by me'-section
+
+ Scenario: In the 'Work packages assigned to me'-Section, I should only see work packages for the selected project
+ And there are the following issues with attributes:
+ | subject | project | author | assignee |
+ | Test-Issue | tested_project | bob | bob |
+ | NO-SHOW | tested_project | bob | mary |
+ And the following widgets are selected for the overview page of the "tested_project" project:
+ | top | work_packages_assigned_to_me |
+ And I am on the homepage for the project "tested_project"
+ Then I should see the widget "work_packages_assigned_to_me"
+ And I should see the work-package-subject "Test-Issue" in the 'Work packages assigned to me'-section
+ And I should not see the work-package-subject "NO-SHOW" in the 'Work packages assigned to me'-section
+
+ Scenario: In the 'Work packages I am responsible for'-Section, I should only see work packages for the selected project
+ And there are the following issues with attributes:
+ | subject | project | author | assignee | responsible |
+ | Test-Issue | tested_project | bob | bob | bob |
+ | NO-SHOW | tested_project | bob | mary | mary |
+ And the following widgets are selected for the overview page of the "tested_project" project:
+ | top | work_packages_responsible_for |
+ And I am on the homepage for the project "tested_project"
+ Then I should see the widget "work_packages_responsible_for"
+ And I should see the work-package-subject "Test-Issue" in the 'Work packages responsible for'-section
+ And I should not see the work-package-subject "NO-SHOW" in the 'Work packages responsible for'-section
+
+ Scenario: In the 'Work packages watched by me'-Section, I should only see work packages for the selected project
+ And there are the following issues with attributes:
+ | subject | project | author | watched_by |
+ | Test-Issue | tested_project | bob | bob |
+ | NOT-WATCHED | other_project | bob | bob,mary |
+ And the following widgets are selected for the overview page of the "tested_project" project:
+ | top | work_packages_watched |
+ And I am on the homepage for the project "tested_project"
+ Then I should see the widget "work_packages_watched"
+ And I should see the work-package-subject "Test-Issue" in the 'Work packages watched'-section
+ And I should not see the work-package-subject "NOT-WATCHED" in the 'Work packages watched'-section
diff --git a/vendored-plugins/openproject-my_project_page/features/project_details_widget.feature b/vendored-plugins/openproject-my_project_page/features/project_details_widget.feature
new file mode 100644
index 0000000000..c0c7dea376
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/project_details_widget.feature
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Project Details Widget
+
+ Background:
+ Given there is 1 project with the following:
+ | Name | Parent |
+ And the project "Parent" has 1 subproject with the following:
+ | Name | Child |
+ And there is a role "Admin"
+ And there is a role "Manager"
+ And I am already Admin
+
+ @javascript
+ Scenario: Adding a "Calendar" widget
+ Given I am on the project "Parent" overview personalization page
+ When I select "Calendar" from "block-select"
+ And I wait for the AJAX requests to finish
+ Then the "Calendar" widget should be in the hidden block
+
+ Scenario: Includes links to all child projects
+ Given the following widgets are selected for the overview page of the "Parent" project:
+ | top | Project_details |
+ When I go to the overview page of the project called "Parent"
+ And I follow "Child" within ".widget-box .project_details"
+ Then I should be on the overview page of the project called "Child"
diff --git a/vendored-plugins/openproject-my_project_page/features/step_definitions/add_widget_steps.rb b/vendored-plugins/openproject-my_project_page/features/step_definitions/add_widget_steps.rb
new file mode 100644
index 0000000000..e17b9f9a3c
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/step_definitions/add_widget_steps.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Then /^"(.+)" should be disabled in the my project page available widgets drop down$/ do |widget_name|
+ option_name = MyProjectsOverviewsController.available_blocks.detect{|_k, v| I18n.t(v) == widget_name}.first.dasherize
+
+ steps %Q{Then the "block-select" drop-down should have the following options disabled:
+ | #{option_name} |}
+end
diff --git a/vendored-plugins/openproject-my_project_page/features/step_definitions/block_steps.rb b/vendored-plugins/openproject-my_project_page/features/step_definitions/block_steps.rb
new file mode 100644
index 0000000000..ba1457adb1
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/step_definitions/block_steps.rb
@@ -0,0 +1,51 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Given /^there is a news "(.+)" for project "(.+)"$/ do |news_title, project_name|
+
+ project = Project.find_by_name(project_name)
+ project.news.create!(title: news_title, description: "lorem ipsum")
+
+end
+
+Then /^there should be (\d+) news$/ do |count|
+ News.count.should eql count.to_i
+end
+
+Then /^there should be (\d+) news for project "(.+)"$/ do |count, project_name|
+ project = Project.find_by_name(project_name)
+ project.news.count.should eql count.to_i
+end
+
+When(/^I should not see the news-headline "([^"]*)"$/) do |news_headline|
+ page.should_not have_css("#block_news_latest .news .overview a", text: news_headline)
+end
+
+When(/^I should see the news-headline "([^"]*)"$/) do |news_headline|
+ page.should have_css("#block_news_latest .news .overview a", text: news_headline)
+end
+
+When /^I should see the work-package-subject "([^"]*)" in the '(.+)'-section$/ do |work_package_subject, section|
+ page.should have_css("\#block_#{section.downcase.tr(' ','_')} td.subject a", text: work_package_subject)
+end
+
+When /^I should not see the work-package-subject "([^"]*)" in the '(.+)'-section$/ do |work_package_subject, section|
+ page.should_not have_css("\#block_#{section.downcase.tr(' ','_')} td.subject a", text: work_package_subject)
+end
diff --git a/vendored-plugins/openproject-my_project_page/features/step_definitions/custom_overview_page_steps.rb b/vendored-plugins/openproject-my_project_page/features/step_definitions/custom_overview_page_steps.rb
new file mode 100644
index 0000000000..48074c9cfb
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/step_definitions/custom_overview_page_steps.rb
@@ -0,0 +1,81 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Given /^I start editing the overview page(?: again)?$/ do
+ driver.find_element(:link, "Personalize this page").click
+ # ERROR: Caught exception [ERROR: Unsupported command [dragAndDropToObject]]
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+end
+
+Then /^I should be able to change things and see my changes when I finish$/ do
+ driver.find_element(:css, "#list-top > #block_workpackagetracking").should be_displayed
+ # ERROR: Caught exception [ERROR: Unsupported command [dragAndDropToObject]]
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ driver.find_element(:css, "#list-hidden > #block_members").should be_displayed
+ # ERROR: Caught exception [ERROR: Unsupported command [select]]
+ driver.find_element(:link, "Add").click
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ (driver.find_elements(:css, "#block-select > option[value='timelog'][disabled]").size).should == 1
+ driver.find_element(:css, "#list-hidden > #block_timelog").should be_displayed
+ # ERROR: Caught exception [ERROR: Unsupported command [dragAndDropToObject]]
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ driver.find_element(:css, "#list-top > #block_timelog").should be_displayed
+ # ERROR: Caught exception [ERROR: Unsupported command [selectWindow]]
+ driver.find_element(:css, "#block_wiki > div > a.icon.icon-delete").click
+ # ERROR: Caught exception [ERROR: Unsupported command [getConfirmation]]
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ (driver.find_elements(:css, "#block-select > option[value='wiki'][disabled]").size).should == 0
+ driver.find_element(:link, "Back").click
+ driver.find_element(:css, "#list-top > .widget-box > .issues.overview").should be_displayed
+ driver.find_element(:css, "#list-top > .widget-box > .total-hours").should be_displayed
+ (driver.find_elements(:css, "#list-left > .widget-box > .wiki").size).should == 0
+end
+
+Then /^I should be able to add a teaser element with custom text$/ do
+ driver.find_element(:link, "Add").click
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ (driver.find_element(:css, "#list-hidden > #block_a > .handle > #a-preview-div > h2").text).should == "Add teaser ..."
+ driver.find_element(:css, "#list-hidden > #block_a > .handle > #a-preview-div").should be_displayed
+ driver.find_element(:link, "Edit").click
+ driver.find_element(:css, "#list-hidden > #block_a > .handle > #a-form-div").should be_displayed
+ driver.find_element(:id, "block_title_a").clear
+ driver.find_element(:id, "block_title_a").send_keys "NewTitle"
+ driver.find_element(:id, "a-form-submit").click
+ (driver.find_element(:css, "#list-hidden > #block_a > .handle > #a-preview-div > h2").text).should == "NewTitle"
+ driver.find_element(:link, "Edit").click
+ driver.find_element(:id, "textile_a").clear
+ driver.find_element(:id, "textile_a").send_keys "NewContent"
+ driver.find_element(:link, "Save").click
+ (driver.find_element(:css, "#list-hidden > #block_a > .handle > #a-preview-div > p").text).should == "NewContent"
+ # ERROR: Caught exception [ERROR: Unsupported command [dragAndDropToObject]]
+ !60.times{ break unless (driver.find_element(:css, "#ajax-indicator").displayed? rescue true); sleep 1 }
+ driver.find_element(:css, "#list-right > #block_a").should be_displayed
+ (driver.find_element(:css, "#list-right > #block_a > .handle > #a-preview-div > p").text).should == "NewContent"
+ (driver.find_element(:css, "#list-right > #block_a > .handle > #a-preview-div > h2").text).should == "NewTitle"
+ driver.find_element(:link, "Back").click
+ (driver.find_element(:css, "#list-right > .widget-box > p").text).should == "NewContent"
+end
+
+Then /^I should be able to delete a teaser element$/ do
+ driver.find_element(:css, "#block_a > div > a.icon.icon-delete").click
+ # ERROR: Caught exception [ERROR: Unsupported command [getConfirmation]]
+ driver.find_element(:link, "Back").click
+ (driver.find_elements(:css, "#list-right > .widget-box > p").size).should == 0
+end
diff --git a/vendored-plugins/openproject-my_project_page/features/step_definitions/disabled_scenarios.rb b/vendored-plugins/openproject-my_project_page/features/step_definitions/disabled_scenarios.rb
new file mode 100644
index 0000000000..fd825bfcc8
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/step_definitions/disabled_scenarios.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+# The plugin changes the project-overview-page, which breaks this Scenario
+ScenarioDisabler.disable(feature: "Showing Projects",
+ scenario: "Calendar link in the 'tickets box' should work when calendar is activated"
+ )
diff --git a/vendored-plugins/openproject-my_project_page/features/step_definitions/selected_widgets_steps.rb b/vendored-plugins/openproject-my_project_page/features/step_definitions/selected_widgets_steps.rb
new file mode 100644
index 0000000000..ec394e03c9
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/step_definitions/selected_widgets_steps.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Given /^the following widgets are selected for the overview page of the "(.+)" project:$/ do |project_name, table|
+ project = Project.find_by_name(project_name)
+ page = MyProjectsOverview.find_or_create_by(project_id: project.id)
+
+ blocks = ({ "top" => "", "left" => "", "right" => "", "hidden" => "" }).merge(table.rows_hash)
+
+ blocks.each { |k, v| page.send((k + "=").to_sym, v.split(",").map{|s| s.strip.downcase}) }
+
+ page.save
+end
+
+Then /^the "(.+)" widget should be in the hidden block$/ do |widget_name|
+ steps %{Then I should see "#{widget_name}" within "#list-hidden"}
+end
diff --git a/vendored-plugins/openproject-my_project_page/features/subprojects_widget.feature b/vendored-plugins/openproject-my_project_page/features/subprojects_widget.feature
new file mode 100644
index 0000000000..aed489b4fc
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/subprojects_widget.feature
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Feature: Subproject Widget
+
+ Background:
+ Given there is 1 project with the following:
+ | Name | Parent |
+ And the project "Parent" has 1 subproject with the following:
+ | Name | Child |
+ And there is a role "Admin"
+ And there is a role "Manager"
+ And I am already Admin
+
+ @javascript
+ Scenario: Adding a "Subproject" widget
+ Given I am on the project "Parent" overview personalization page
+ When I select "Subprojects" from "block-select"
+ And I wait for the AJAX requests to finish
+ Then the "Subprojects" widget should be in the hidden block
+ And "Subprojects" should be disabled in the my project page available widgets drop down
+
+ Scenario: Includes links to all child projects
+ Given the following widgets are selected for the overview page of the "Parent" project:
+ | top | Subprojects |
+ And I am on the homepage for the project "Parent"
+ And I follow "Child" within ".widget-box .subprojects"
+ Then I should be on the overview page of the project called "Child"
diff --git a/vendored-plugins/openproject-my_project_page/features/support/path.rb b/vendored-plugins/openproject-my_project_page/features/support/path.rb
new file mode 100644
index 0000000000..24f48a7d53
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/features/support/path.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module ProjectPageNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the project "(.+)" overview personalization page$/
+ project = Project.find_by_name($1)
+ "/my_projects_overview/#{project.identifier}/page_layout"
+ else
+ super
+ end
+ end
+end
+
+World(ProjectPageNavigationHelpers)
diff --git a/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page.rb b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page.rb
new file mode 100644
index 0000000000..adaf682258
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module MyProjectPage
+ require "open_project/my_project_page/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/engine.rb b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/engine.rb
new file mode 100644
index 0000000000..0bd12467ca
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/engine.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/plugins'
+
+require 'rails/engine'
+require 'open_project/my_project_page/plugin_blocks'
+
+module OpenProject::MyProjectPage
+ class Engine < ::Rails::Engine
+ engine_name :openproject_my_project_page
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-my_project_page',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0' do
+
+ project_module :my_project_page do
+ Redmine::AccessControl.permission(:view_project).actions << "my_projects_overviews/index" <<
+ "my_projects_overviews/show_all_members"
+ Redmine::AccessControl.permission(:edit_project).actions << "my_projects_overviews/page_layout" <<
+ "my_projects_overviews/add_block" <<
+ "my_projects_overviews/remove_block" <<
+ "my_projects_overviews/update_custom_element" <<
+ "my_projects_overviews/order_blocks" <<
+ "my_projects_overviews/destroy_attachment"
+ end
+ end
+
+ assets %w(my_project_page/my_projects_overview.css my_project_page/my_project_page.js)
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/plugin_blocks.rb b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/plugin_blocks.rb
new file mode 100644
index 0000000000..2669c5fa2f
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/plugin_blocks.rb
@@ -0,0 +1,48 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+module OpenProject
+ module MyProjectPage
+ # This method loads additional blocks for the myproject-page from registered pugins
+ def self.plugin_blocks
+ #look at the gemspecs of all plugins trying to find views in a /my_project_page/blocks subdirectory
+ @@additional_blocks ||= Dir.glob(
+ Redmine::Plugin.registered_plugins.map do |plugin_id,_|
+ gem_name = plugin_id.to_s.gsub('openproject_','openproject-') if plugin_id.to_s.starts_with?('openproject_')
+ gem_spec = Gem.loaded_specs[gem_name]
+ if gem_spec.nil?
+ error = "No Gemspec found for plugin: " + plugin_id.to_s \
+ + ", expected gem name to match the plugin name but starting with openproject-"
+ ActiveSupport::Deprecation.warn(error)
+ nil
+ else
+ gem_spec.full_gem_path + '/app/views/my_projects_overviews/blocks/_*.{rhtml,erb}'
+ end
+ end.compact
+ ).inject({}) do |h,file|
+ name = File.basename(file).split('.').first.gsub(/^_/, '')
+ h[name] = ("label_"+ name).to_sym
+ h
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/version.rb b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/version.rb
new file mode 100644
index 0000000000..a2f4e13c37
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/lib/open_project/my_project_page/version.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module MyProjectPage
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/lib/openproject-my_project_page.rb b/vendored-plugins/openproject-my_project_page/lib/openproject-my_project_page.rb
new file mode 100644
index 0000000000..3474db39d2
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/lib/openproject-my_project_page.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+require 'open_project/my_project_page'
diff --git a/vendored-plugins/openproject-my_project_page/openproject-my_project_page.gemspec b/vendored-plugins/openproject-my_project_page/openproject-my_project_page.gemspec
new file mode 100644
index 0000000000..1c4a9489d5
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/openproject-my_project_page.gemspec
@@ -0,0 +1,27 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "open_project/my_project_page/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-my_project_page"
+ s.version = OpenProject::MyProjectPage::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/my-project-page"
+ s.summary = 'OpenProject My Project Page'
+ s.description = "This plugin replaces the old overview page for projects with something similar to the 'My Page':
+ The provided blocks are scoped to the project and show only information (open tickets, News etc.).
+ It also provides a mechanism to add custom content-blocks(teasers) for the Project."
+ s.license = "GPLv3"
+
+ s.files = Dir["{app,config,db,lib}/**/*", "README.md"]
+ s.test_files = Dir["spec/**/*"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+
+ s.add_development_dependency "factory_girl_rails", "~> 4.0"
+end
diff --git a/vendored-plugins/openproject-my_project_page/spec/controllers/my_projects_overview_controller_spec.rb b/vendored-plugins/openproject-my_project_page/spec/controllers/my_projects_overview_controller_spec.rb
new file mode 100644
index 0000000000..b60c162370
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/controllers/my_projects_overview_controller_spec.rb
@@ -0,0 +1,65 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MyProjectsOverviewsController, type: :controller do
+ before :each do
+ allow(@controller).to receive(:set_localization)
+ expect(@controller).to receive(:authorize)
+
+ @role = FactoryGirl.create(:non_member)
+ @user = FactoryGirl.create(:admin)
+
+ allow(User).to receive(:current).and_return @user
+
+ @params = {}
+ end
+
+ let(:project) { FactoryGirl.create(:project) }
+
+ describe 'index' do
+ let(:params) { { "id" => project.id.to_s } }
+
+ describe "WHEN calling the page" do
+ render_views
+
+ before do
+ get 'index', params
+ end
+
+ it 'renders the overview page' do
+ expect(response).to be_success
+ expect(response).to render_template 'index'
+ end
+ end
+
+ describe "WHEN calling the page
+ WHEN providing a jump parameter" do
+
+ before do
+ params["jump"] = "work_packages"
+ get 'index', params
+ end
+
+ it { expect(response).to redirect_to project_work_packages_path(project) }
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/spec/disabled_specs.rb b/vendored-plugins/openproject-my_project_page/spec/disabled_specs.rb
new file mode 100644
index 0000000000..8fcf716951
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/disabled_specs.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require "rspec/example_disabler"
+
+# TODO does rspec add a space randomly to the metadata?!
+# (better make the example-disabler a litte more resilient against this)
+RSpec::ExampleDisabler.disable_example(
+ 'ProjectsController show ',
+ "plugin openproject-my_project_overview overwrites routes for show."
+)
+
+RSpec::ExampleDisabler.disable_example(
+ 'ProjectsController show',
+ "plugin openproject-my_project_overview overwrites routes for show."
+)
diff --git a/vendored-plugins/openproject-my_project_page/spec/lib/redmine/access_control_spec.rb b/vendored-plugins/openproject-my_project_page/spec/lib/redmine/access_control_spec.rb
new file mode 100644
index 0000000000..1d3ba47057
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/lib/redmine/access_control_spec.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Redmine::AccessControl do
+ let(:view_project_permission) { Redmine::AccessControl.permission(:view_project) }
+ let(:edit_project_permission) { Redmine::AccessControl.permission(:edit_project) }
+
+ describe '#view_project' do
+ it { expect(view_project_permission.actions).to be_include("my_projects_overviews/index") }
+ end
+
+ describe '#edit_project' do
+ it { expect(edit_project_permission.actions).to be_include("my_projects_overviews/page_layout") }
+ it { expect(edit_project_permission.actions).to be_include("my_projects_overviews/add_block") }
+ it { expect(edit_project_permission.actions).to be_include("my_projects_overviews/update_custom_element") }
+ it { expect(edit_project_permission.actions).to be_include("my_projects_overviews/order_blocks") }
+ it { expect(edit_project_permission.actions).to be_include("my_projects_overviews/destroy_attachment") }
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/spec/models/my_projects_overview_spec.rb b/vendored-plugins/openproject-my_project_page/spec/models/my_projects_overview_spec.rb
new file mode 100644
index 0000000000..f7853113d9
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/models/my_projects_overview_spec.rb
@@ -0,0 +1,78 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe MyProjectsOverview, type: :model do
+ before do
+ @enabled_module_names = %w[activity work_package_tracking news wiki]
+ FactoryGirl.create(:project, enabled_module_names: @enabled_module_names)
+ @project = Project.first
+ @overview = MyProjectsOverview.create(project_id: @project.id)
+ end
+
+ it 'sets default elements for new records if no elements are provided' do
+ o = MyProjectsOverview.new
+ expect(o.left).to match_array(["project_description", "project_details", "work_package_tracking"])
+ expect(o.right).to match_array(["members", "news_latest"])
+ expect(o.top).to match_array([])
+ expect(o.hidden).to match_array([])
+ end
+
+ it 'does not set default elements if elements are provided' do
+ o = MyProjectsOverview.new left: ["members"]
+ expect(o.left).to match_array(["members"])
+ expect(o.right).to match_array(["members", "news_latest"])
+ expect(o.top).to match_array([])
+ expect(o.hidden).to match_array([])
+ end
+
+
+ it 'does not enforce default elements' do
+ @overview.right = []
+ @overview.save!
+
+ @overview.reload
+ expect(@overview.right).to match_array([])
+ end
+
+ it 'creates a new custom element' do
+ expect(@overview.new_custom_element).not_to be_nil
+ end
+
+ it "creates a new custom element as [idx, title, text]" do
+ ce = @overview.new_custom_element
+ expect(ce[0]).to eq("a")
+ expect(ce[1]).to be_kind_of String
+ expect(ce[2]).to match(/^h3\./)
+ end
+
+ it "can save a custom element" do
+ @overview.hidden << @overview.new_custom_element
+ ce = @overview.custom_elements.last
+ expect(@overview.save_custom_element(ce[0], "Title", "Content")).to be true
+ expect(ce[1]).to eq("Title")
+ expect(ce[2]).to eq("Content")
+ end
+
+ it "should always show attachments" do
+ expect(@overview.attachments_visible?(nil)).to be true
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/spec/routing/my_projects_page_routing_spec.rb b/vendored-plugins/openproject-my_project_page/spec/routing/my_projects_page_routing_spec.rb
new file mode 100644
index 0000000000..1f6b826718
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/routing/my_projects_page_routing_spec.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'spec_helper'
+
+describe MyProjectsOverviewsController, type: :routing do
+ describe "routing" do
+ describe "overview-page" do
+ it {
+ expect(get('/projects/test-project')).to route_to(controller: 'my_projects_overviews',
+ action: 'index',
+ id: 'test-project')
+ }
+
+ # make sure that the mappings are not greedy
+ it {
+ expect(get('/projects/new')).to route_to(controller: 'projects',
+ action: 'new')
+ }
+
+ it {
+ expect(get('/projects/test-project/settings')).to route_to(controller: 'projects',
+ action: 'settings',
+ id: 'test-project')
+ }
+
+ end
+
+
+ end
+end
diff --git a/vendored-plugins/openproject-my_project_page/spec/spec_helper.rb b/vendored-plugins/openproject-my_project_page/spec/spec_helper.rb
new file mode 100644
index 0000000000..9bcf166dd7
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/spec_helper.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+RAILS_ENV = "test" unless defined? RAILS_ENV
+
+require "spec_helper"
+Dir[File.dirname(__FILE__) + '/support/*.rb'].each {|file| require file }
diff --git a/vendored-plugins/openproject-my_project_page/spec/support/plugin_spec_helper.rb b/vendored-plugins/openproject-my_project_page/spec/support/plugin_spec_helper.rb
new file mode 100644
index 0000000000..b6b69684b8
--- /dev/null
+++ b/vendored-plugins/openproject-my_project_page/spec/support/plugin_spec_helper.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject My Project Page Plugin
+#
+# Copyright (C) 2011-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module PluginSpecHelper
+ # add helper-functions for this plugin here
+end
diff --git a/vendored-plugins/openproject-openid_connect/.hound.yml b/vendored-plugins/openproject-openid_connect/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-openid_connect/.rubocop.yml b/vendored-plugins/openproject-openid_connect/.rubocop.yml
new file mode 100644
index 0000000000..a22df7c695
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - *.gemspec
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
\ No newline at end of file
diff --git a/vendored-plugins/openproject-openid_connect/CHANGELOG.md b/vendored-plugins/openproject-openid_connect/CHANGELOG.md
new file mode 100644
index 0000000000..5f82f18c24
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## 0.1.0
+
+* `#5558` use openproject-auth_plugins as basis
+
+## 0.0.1
+
+* `#5555` Multi-Provider login screens
diff --git a/vendored-plugins/openproject-openid_connect/README.md b/vendored-plugins/openproject-openid_connect/README.md
new file mode 100644
index 0000000000..aa76aaef6b
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/README.md
@@ -0,0 +1,131 @@
+# OpenProject OpenID Connect Plugin
+
+Adds support for OmniAuth OpenID Connect strategy providers, most importantly Google.
+
+## Dependencies
+
+You will have to add the following lines to your OpenProject's _Gemfile.plugins_ for the time being (omit aleady existing ones):
+
+ gem "openproject-auth_plugins", :git => 'git@github.com:finnlabs/openproject-auth_plugins', :branch => 'dev'
+ gem 'omniauth-openid-connect', :git => 'git@github.com:finnlabs/omniauth-openid-connect.git', :branch => 'dev'
+ gem 'omniauth-openid_connect-providers', :git => 'git@github.com:finnlabs/omniauth-openid_connect-providers.git', :branch => 'dev'
+ gem 'openproject-openid_connect', :git => 'git@github.com:finnlabs/openproject-openid_connect.git', :branch => 'dev'
+
+ gem 'lobby_boy', :git => 'git@github.com:finnlabs/lobby_boy.git', :branch => 'dev'
+
+### Development
+
+If you want to run the tests you will have add the following as well:
+
+ group :test do
+ gem 'rspec-steps', '~> 0.4.0'
+ end
+
+## Configuration
+
+The provider configuration can be either done in `configuration.yml` or within the settings.
+
+### configuration.yml
+
+Example configuration:
+
+ default:
+ openid_connect:
+ google:
+ identifier: "9295222hfbiu2btgu3b4i.apps.googleusercontent.com"
+ secret: "4z389thugh334t8h"
+ icon: "openid_connect/auth_provider-google.png"
+ display_name: "Google"
+
+The last two attributes are commonly available for all providers.
+They are used to change a provider's look.
+
+Note that `openid_connect/auth_provider-google.png` is the one custom provider icon this plugin has out of the box. Other icons you will have to add yourself.
+
+`display_name` changes a provider's label shown to the user.
+
+### Single Sign-On
+
+This plugin supports OpenID Connect Session Management. To setup a provider for SSO
+you have to configure the following additional options for the provider:
+
+```yaml
+# example settings for openproject.com
+sso: true
+issuer: 'https://login.openproject.com'
+discovery: false
+end_session_endpoint: '/auth/end_session'
+check_session_iframe: '/auth/check_session'
+```
+
+### Settings
+
+There is no UI for the settings just yet. One way to set them until then is the rails console:
+
+ Setting["plugin_openproject_openid_connect"] = {
+ "providers" => {
+ "google" => {
+ "identifier" => "9295222hfbiu2btgu3b4i.apps.googleusercontent.com",
+ "secret" => "4z389thugh334t8h"
+ },
+ "heroku" => {
+ "identifier" => "foobar",
+ "secret" => "baz"
+ }
+ }
+ }
+
+While Google and Heroku are pre-defined you can add arbitrary providers through configuration.
+Those may then require the host and/or endpoints to be specified depending on whether or not a particular provider adheres to the default endpoint paths.
+
+ Setting["plugin_openproject_openid_connect"] = {
+ "providers" => {
+ "myprovider" => {
+ "host" => "login.myprovider.net",
+ "identifier" => "9295222hfbiu2btgu3b4i.apps.googleusercontent.com",
+ "secret" => "4z389thugh334t8h"
+ },
+ "yourprovider" => {
+ "identifier" => "foobar",
+ "secret" => "baz",
+ "authorization_endpoint" => "https://auth.yourprovider.com/oauth2/authorize"
+ "token_endpoint" => "https://auth.yourprovider.com/oauth2/token?api-version=1.0"
+ "userinfo_endpoint" => "https://users.yourprovider.com/me"
+ },
+ "theirprovider" => {
+ "identifier" => "foobar",
+ "secret" => "baz",
+ "host" => "signin.theirprovider.co.uk",
+ "authorization_endpoint" => "/oauth2/authorization/new"
+ "token_endpoint" => "/oauth2/tokens"
+ "userinfo_endpoint" => "/oauth2/users/me"
+ }
+ }
+ }
+
+If a host is given, relative endpoint paths will refer to said host.
+No host is required if absolute endpoint URIs are given.
+
+The configuration of the pre-defined providers (currently Google and Heroku) can be overriden as well.
+
+## Provider Client Registration
+
+Client ID and secret are often provided by the provider, otherwise refer to the provider on how to create them.
+
+Use the following scheme for creating a callback URL (you have to whitelist that URL at the provider):
+
+ https://YOURAPP.example.org/auth/PROVIDER_NAME/callback
+
+Replace `PROVIDER_NAME` with the key you used for the provider in the settings hash. So e.g. for an app running on openproject.example.org and authentication via Google, you can set up the following callback URL:
+
+ https://openproject.example.org/auth/google/callback
+
+## Provider SSL certificate validation
+
+This plugin uses OpenSSL's default certificate store (on Linux you can ususally find it in `/etc/ssl/certs`).
+
+If you want to use a different list of CAs for validating provider SSL certificates, you can set the environment variable `SSL_CERT_DIR` to another path containing CA certificates. Note that this environment variable is an OpenSSL feature, so it changes the CA list for all libraries using OpenSSL that don't explicitly specify another path.
+
+## Credits
+
+This plugin uses some of Neil Hainsworth' [Free Social Icons](http://www.neilorangepeel.com/free-social-icons/).
diff --git a/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/LICENSE b/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/LICENSE
new file mode 100644
index 0000000000..b12fb6a6a2
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/LICENSE
@@ -0,0 +1,5 @@
+Free Social Icons from Neil Hainsworth
+
+"Feel free to use them however you'd like :)"
+
+http://www.neilorangepeel.com/free-social-icons/
diff --git a/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/auth_provider-google.png b/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/auth_provider-google.png
new file mode 100644
index 0000000000..08094966f2
Binary files /dev/null and b/vendored-plugins/openproject-openid_connect/app/assets/images/openid_connect/auth_provider-google.png differ
diff --git a/vendored-plugins/openproject-openid_connect/app/session/session_controller.rb b/vendored-plugins/openproject-openid_connect/app/session/session_controller.rb
new file mode 100644
index 0000000000..aa32727393
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/app/session/session_controller.rb
@@ -0,0 +1,19 @@
+class SessionController < ActionController::Base
+ def logout_warning
+ url = signin_url back_url: params[:back_url]
+
+ render 'logout_warning', locals: { message: link_i18n(:logout_warning, url) }
+ end
+
+ private
+
+ ##
+ # Finds any words enclosed in brackets (like links in Markdown) and
+ # turns them into a link with the given URL.
+ def link_i18n(i18n_key, url)
+ text = translate i18n_key
+ html = text.gsub /\[([^\[\]]+)\]/, "\\1 "
+
+ html.html_safe
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/app/views/session/_warn_logout.js.erb b/vendored-plugins/openproject-openid_connect/app/views/session/_warn_logout.js.erb
new file mode 100644
index 0000000000..17fffe8f24
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/app/views/session/_warn_logout.js.erb
@@ -0,0 +1,16 @@
+// window.parent == OpenProject window
+var op = window.parent;
+var $ = op.jQuery
+var back_url = encodeURIComponent(op.document.location.href);
+
+$.ajax({
+ url: "/session/logout_warning?back_url=" + back_url,
+ cache: false,
+ success: function(html) {
+ $("#logout-warning").remove();
+ $(html).prependTo("body").hide().slideDown();
+ $('html, body').animate({
+ scrollTop: $("#logout-warning").offset().top
+ }, 1000);
+ }
+});
diff --git a/vendored-plugins/openproject-openid_connect/app/views/session/logout_warning.html.erb b/vendored-plugins/openproject-openid_connect/app/views/session/logout_warning.html.erb
new file mode 100644
index 0000000000..5fe0c74105
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/app/views/session/logout_warning.html.erb
@@ -0,0 +1,6 @@
+
+
<%= t('title.warning') %>
+
+ <%= message %>
+
+
diff --git a/vendored-plugins/openproject-openid_connect/config/locales/de.yml b/vendored-plugins/openproject-openid_connect/config/locales/de.yml
new file mode 100644
index 0000000000..5be4c56c91
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/config/locales/de.yml
@@ -0,0 +1,4 @@
+de:
+ logout_warning: >
+ Sie wurden ausgeloggt. Inhalte von Formularen, die sie abschicken möchten,
+ können verloren gehen. Bitte [loggen Sie sich wieder ein].
diff --git a/vendored-plugins/openproject-openid_connect/config/locales/en.yml b/vendored-plugins/openproject-openid_connect/config/locales/en.yml
new file mode 100644
index 0000000000..f7768f9e45
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/config/locales/en.yml
@@ -0,0 +1,4 @@
+en:
+ logout_warning: >
+ You have been logged out. The contents of any form you submit may be lost.
+ Please [log in].
diff --git a/vendored-plugins/openproject-openid_connect/config/routes.rb b/vendored-plugins/openproject-openid_connect/config/routes.rb
new file mode 100644
index 0000000000..8631942509
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/config/routes.rb
@@ -0,0 +1,7 @@
+require 'lobby_boy'
+
+Rails.application.routes.draw do
+ mount LobbyBoy::Engine, at: '/'
+
+ get '/session/logout_warning', to: 'session#logout_warning'
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/hooks/session_iframes.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/hooks/session_iframes.rb
new file mode 100644
index 0000000000..45cece17f1
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/hooks/session_iframes.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+module OpenProject
+ class Hooks::SessionIFrames < Redmine::Hook::ViewListener
+ render_on :view_layouts_base_body_bottom, partial: 'lobby_boy/iframes'
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect.rb
new file mode 100644
index 0000000000..fa4c11393c
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module OpenIDConnect
+ require 'open_project/openid_connect/engine'
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/engine.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/engine.rb
new file mode 100644
index 0000000000..4af7c4a365
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/engine.rb
@@ -0,0 +1,107 @@
+require 'open_project/openid_connect/lobby_boy_configuration'
+require 'open_project/plugins'
+require 'lobby_boy'
+
+module OpenProject::OpenIDConnect
+ class Engine < ::Rails::Engine
+ engine_name :openproject_openid_connect
+
+ include OpenProject::Plugins::ActsAsOpEngine
+ extend OpenProject::Plugins::AuthPlugin
+
+ register 'openproject-openid_connect',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 3.1.0pre1',
+ settings: { 'default' => { 'providers' => {} } }
+
+ assets %w(
+ openid_connect/auth_provider-google.png
+ )
+
+ register_auth_providers do
+ require 'omniauth/openid_connect/providers'
+
+ Providers = OmniAuth::OpenIDConnect::Providers
+
+ # Use OpenSSL default certificate store instead of HTTPClient's.
+ # It's outdated and it's unclear how it's managed.
+ OpenIDConnect.http_config do |config|
+ config.ssl_config.set_default_paths
+ end
+
+ def configuration
+ from_settings = if Setting.plugin_openproject_openid_connect.is_a? Hash
+ Hash(Setting.plugin_openproject_openid_connect["providers"])
+ else
+ {}
+ end
+ # Settings override configuration.yml
+ Hash(OpenProject::Configuration["openid_connect"]).deep_merge(from_settings)
+ end
+
+ Providers.configure custom_options: [
+ :display_name?, :icon?, :sso?, :issuer?,
+ :check_session_iframe?, :end_session_endpoint?
+ ]
+
+ strategy :openid_connect do
+ # update base redirect URI in case settings changed
+ Providers.configure base_redirect_uri: "#{Setting.protocol}://#{Setting.host_name}"
+ Providers.load(configuration).map(&:to_h)
+ end
+ end
+
+ config.to_prepare do
+ OpenProject::OpenIDConnect::LobbyBoyConfiguration.update!
+
+ if LobbyBoy.configured?
+ require 'open_project/hooks/session_iframes'
+
+ require 'open_project/openid_connect/sso_login'
+ ::Concerns::OmniauthLogin.prepend SSOLogin
+
+ require 'open_project/openid_connect/sso_logout'
+ ::AccountController.prepend SSOLogout
+ end
+ end
+
+ initializer "openid_connect.middleware.lobby_boy_config" do |app|
+ anchor =
+ if defined? ::Multitenancy::Elevators::MappedDomainElevator
+ ::Multitenancy::Elevators::MappedDomainElevator
+ else
+ ActionDispatch::Callbacks
+ end
+
+ app.config.middleware.insert_after anchor, OpenProject::OpenIDConnect::LobbyBoyConfiguration
+ end
+
+ config.to_prepare do
+ # set a secure cookie in production
+ secure_cookie = !!Rails.configuration.force_ssl
+
+ # register an #after_login callback which sets a cookie containing the access token
+ OpenProject::OmniAuth::Authorization.after_login do |_user, auth_hash, context|
+ # check the configuration
+ if store_access_token?
+ # fetch the access token if it's present
+ access_token = auth_hash.fetch(:credentials, {})[:token]
+ # put it into a cookie
+ if context && access_token
+ context.send(:cookies)[:_open_project_session_access_token] = {
+ value: access_token,
+ secure: secure_cookie
+ }
+ end
+ end
+ end
+
+ # for changing the setting at runtime, e.g. for testing, we need to evaluate this each time
+ def self.store_access_token?
+ # TODO: we might want this to be configurable, for now we always enable it
+ # OpenProject::Configuration['omniauth_store_access_token_in_cookie']
+ true
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/lobby_boy_configuration.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/lobby_boy_configuration.rb
new file mode 100644
index 0000000000..2c939480dc
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/lobby_boy_configuration.rb
@@ -0,0 +1,62 @@
+module OpenProject
+ module OpenIDConnect
+ class LobbyBoyConfiguration
+ def initialize(app)
+ @app = app
+ end
+
+ ##
+ # Updates the lobby boy settings with each request in case
+ # the host or protocol change in OpenProject's settings.
+ def call(env)
+ self.class.update_client! if LobbyBoy.configured?
+
+ @app.call env
+ end
+
+ class << self
+ attr_accessor :provider
+
+ def enabled?
+ Rails.env != 'test'
+ end
+
+ def update!
+ self.provider = lookup_provider
+
+ if enabled? && provider
+ update_client!
+ update_provider!
+ end
+ end
+
+ def lookup_provider
+ OpenProject::Plugins::AuthPlugin.providers.find { |p| p[:sso] }
+ end
+
+ def update_client!
+ LobbyBoy.configure_client! host: host,
+ logged_in: lambda { !session[:user_id].nil? },
+ end_session_endpoint: end_session_endpoint,
+ on_logout_js_partial: 'session/warn_logout'
+ end
+
+ def update_provider!
+ LobbyBoy.configure_provider! name: provider[:name],
+ client_id: provider[:client_options][:identifier],
+ issuer: provider[:issuer],
+ end_session_endpoint: provider[:end_session_endpoint],
+ check_session_iframe: provider[:check_session_iframe]
+ end
+
+ def host
+ "#{Setting.protocol}://#{Setting.host_name}"
+ end
+
+ def end_session_endpoint
+ '/logout?script'
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_login.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_login.rb
new file mode 100644
index 0000000000..2f5641a33f
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_login.rb
@@ -0,0 +1,13 @@
+module OpenProject
+ module OpenIDConnect
+ module SSOLogin
+ include ::LobbyBoy::SessionHelper
+
+ def authorization_successful(_user, auth_hash)
+ super.tap do |_|
+ confirm_login! # lobby_boy helper
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_logout.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_logout.rb
new file mode 100644
index 0000000000..eb3e422be0
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/sso_logout.rb
@@ -0,0 +1,43 @@
+module OpenProject
+ module OpenIDConnect
+ module SSOLogout
+ include LobbyBoy::SessionHelper
+
+ def session_expired?
+ super || (current_user.logged? && id_token_expired?)
+ end
+
+ ##
+ # Upon reauthentication just return directly with HTTP 200 OK
+ # and do not reset the session.
+ # If not call super which will reset the session, set
+ # the new user, and redirect to some page the script the
+ # reauthentication doesn't care about.
+ def successful_authentication(user)
+ if reauthentication?
+ finish_reauthentication!
+ else
+ super
+ end
+ end
+
+ def logout
+ if params.include? :script
+ logout_user
+
+ return finish_logout!
+ end
+
+ # If the user may view the site without being logged in we redirect back to it.
+ site_open = !(Setting.login_required? && ::Concerns::OmniauthLogin.direct_login?)
+ return_url = site_open && "#{Setting.protocol}://#{Setting.host_name}"
+
+ if logout_at_op! return_url
+ logout_user
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/version.rb b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/version.rb
new file mode 100644
index 0000000000..dc6170ee96
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/open_project/openid_connect/version.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module OpenIDConnect
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/lib/openproject-openid_connect.rb b/vendored-plugins/openproject-openid_connect/lib/openproject-openid_connect.rb
new file mode 100644
index 0000000000..66b53f45ae
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/lib/openproject-openid_connect.rb
@@ -0,0 +1 @@
+require 'open_project/openid_connect'
diff --git a/vendored-plugins/openproject-openid_connect/openproject-openid_connect.gemspec b/vendored-plugins/openproject-openid_connect/openproject-openid_connect.gemspec
new file mode 100644
index 0000000000..1476259ead
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/openproject-openid_connect.gemspec
@@ -0,0 +1,24 @@
+# encoding: UTF-8
+$:.push File.expand_path('../lib', __FILE__)
+
+require 'open_project/openid_connect/version'
+
+Gem::Specification.new do |s|
+ s.name = 'openproject-openid_connect'
+ s.version = OpenProject::OpenIDConnect::VERSION
+ s.authors = 'OpenProject GmbH'
+ s.email = 'info@openproject.com'
+ s.homepage = 'https://community.openproject.org/projects/openid-connect' # TODO check this URL
+ s.summary = 'OpenProject OpenID Connect'
+ s.description = 'Adds OmniAuth OpenID Connect strategy providers to Openproject.'
+ s.license = 'GPLv3'
+
+ s.files = Dir['{app,config,db,lib}/**/*'] + %w(CHANGELOG.md README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency 'openproject-auth_plugins', '~> 5.0.1'
+ s.add_dependency 'omniauth-openid_connect-providers', '~> 0.1'
+ s.add_dependency 'lobby_boy', '~> 0.1'
+
+ s.add_development_dependency 'rspec', '~> 2.99'
+end
diff --git a/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec.rb b/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec.rb
new file mode 100644
index 0000000000..89bc409807
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec.rb
@@ -0,0 +1,171 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+require_relative 'openid_connect_spec_helpers'
+
+RSpec.configure do |c|
+ c.include OpenIDConnectSpecHelpers
+end
+
+describe 'OpenID Connect' do
+ let(:host) { OmniAuth::OpenIDConnect::Heroku.new('foo', {}).host }
+ let(:user_info) do
+ {
+ sub: '87117114115116',
+ name: 'Hans Wurst',
+ email: 'h.wurst@finn.de',
+ given_name: 'Hans',
+ family_name: 'Wurst'
+ }
+ end
+
+ before do
+ # The redirect will include an authorisation code.
+ # Since we don't actually get a valid code in the test we will stub the resulting AccessToken.
+ allow_any_instance_of(OpenIDConnect::Client).to receive(:access_token!) do
+ OpenIDConnect::AccessToken.new client: self, access_token: 'foo bar baz'
+ end
+
+ # Using the granted AccessToken the client then performs another request to the OpenID Connect
+ # provider to retrieve user information such as name and email address.
+ # Since the test is not supposed to make an actual call it is be stubbed too.
+ allow_any_instance_of(OpenIDConnect::AccessToken).to receive(:userinfo!).and_return(
+ OpenIDConnect::ResponseObject::UserInfo.new(user_info))
+
+ # enable storing the access token in a cookie
+ OpenProject::Configuration['omniauth_store_access_token_in_cookie'] = true
+ end
+
+ describe 'sign-up and login' do
+ before do
+ allow(Setting).to receive(:plugin_openproject_openid_connect).and_return(
+
+ 'providers' => {
+ 'heroku' => {
+ 'identifier' => 'does not',
+ 'secret' => 'matter'
+ }
+ }
+
+ )
+ end
+
+ after(:all) do
+ User.delete_all
+ end
+
+ after do
+ User.current = nil
+ end
+
+ it 'works' do
+ ##
+ # it should redirect to the provider's openid connect authentication endpoint
+ click_on_signin
+
+ expect(response.status).to be 302
+ expect(response.location).to match /https:\/\/#{host}.*$/
+
+ params = Rack::Utils.parse_nested_query(response.location.gsub(/^.*\?/, ''))
+
+ expect(params).to include 'client_id'
+ expect(params['redirect_uri']).to match /^.*\/auth\/heroku\/callback$/
+ expect(params['scope']).to include 'openid'
+
+ ##
+ # it should redirect back from the provider to the login page
+ redirect_from_provider
+
+ expect(response.status).to be 302
+ expect(response.location).to match /\/login$/
+
+ ##
+ # it should have created an account waiting to be activated
+ expect(flash[:notice]).to match /account.*created/
+
+ user = User.find_by_mail(user_info[:email])
+
+ expect(user).not_to be nil
+ expect(user.active?).to be false
+
+ ##
+ # it should redirect to the provider again upon clicking on sign-in when the user has been activated
+ user = User.find_by_mail(user_info[:email])
+ user.activate
+ user.save!
+
+ click_on_signin
+
+ expect(response.status).to be 302
+ expect(response.location).to match /https:\/\/#{host}.*$/
+
+ ##
+ # it should then login the user upon the redirect back from the provider
+ redirect_from_provider
+
+ expect(response.status).to be 302
+ expect(response.location).to match /my\/first_login$/
+
+ # after_login requires the optional third context parameter
+ # remove this guard once we are on v4.1
+ if OpenProject::OmniAuth::Authorization.method(:after_login!).arity.abs > 2
+ # check that cookie is stored in the access token
+ expect(response.cookies['_open_project_session_access_token']).to eq 'foo bar baz'
+ end
+ end
+ end
+
+ context 'provider configuration through the settings' do
+ it 'should make providers that are not configured unavailable' do
+ get '/login'
+ expect(response.body).not_to match /Google/i
+
+ expect { click_on_signin('google') }.to raise_error(ActionController::RoutingError)
+ end
+
+ it 'should make providers that have been configured through settings available without requiring a restart' do
+ allow(Setting).to receive(:plugin_openproject_openid_connect).and_return(
+
+ 'providers' => {
+ 'google' => {
+ 'identifier' => 'does not',
+ 'secret' => 'matter'
+ }
+ }
+
+ )
+
+ get '/login'
+ expect(response.body).to match /Google/i
+
+ expect { click_on_signin('google') }.not_to raise_error
+ expect(response.status).to be 302
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec_helpers.rb b/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec_helpers.rb
new file mode 100644
index 0000000000..d26b7c81bd
--- /dev/null
+++ b/vendored-plugins/openproject-openid_connect/spec/requests/openid_connect_spec_helpers.rb
@@ -0,0 +1,13 @@
+module OpenIDConnectSpecHelpers
+ def redirect_from_provider(name = 'heroku')
+ # Emulate the provider's redirect with a nonsense code.
+ get "/auth/#{name}/callback",
+ :code => 'foobar',
+ :redirect_uri => "http://localhost:3000/auth/#{name}/callack"
+ end
+
+ def click_on_signin(pro_name = 'heroku')
+ # Emulate click on sign-in for that particular provider
+ get "/auth/#{pro_name}"
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/README.md b/vendored-plugins/openproject-pdf_export/README.md
new file mode 100644
index 0000000000..3661ae08f1
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/README.md
@@ -0,0 +1,180 @@
+OpenProject PDF Export Plugin
+===========================
+
+This Plugin adds features which enable the configuration and generation of printable export card PDFs,
+especially useful to export story cards from the plugin "OpenProject Backlogs":https://www.openproject.org/projects/plugin-backlogs
+
+
+Requirements
+------------
+
+The OpenProject PDF Export plugin requires the [OpenProject Core](https://github.com/opf/openproject/) in
+version greater or equal to *3.0.0*.
+
+Tests for this plugin require `pdf-inspector`, so just add the following line to
+OpenProject's `Gemfile.plugin` :
+
+`gem "pdf-inspector", "~>1.0.0", :group => :test`
+
+
+Installation
+------------
+
+For OpenProject PDF Export itself you need to add the following line to the
+`Gemfile.plugins` of the project which is using the plugin (if you use a different OpenProject version than OpenProject 4.1, adapt `:branch => "stable/4.1"` to your OpenProject version):
+
+`gem "openproject-pdf_export", git: "https://github.com/finnlabs/openproject-pdf_export.git", :branch => "stable/4.1"`
+
+Afterwards, run:
+
+`bundle install`
+
+This plugin contains migrations. To migrate the database, run:
+
+`rake db:migrate`
+
+There is a seed file to load a default ExportCardConfiguration model, although this is not strictly required.
+
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "openproject-pdf_export", git: "https://github.com/finnlabs/openproject-pdf_export.git", :branch => "stable/4.1"`
+
+from the file `Gemfile.plugins` and run:
+
+`bundle install`
+
+Please not that this leaves plugin data in the database. Currently, we do not
+support full uninstall of the plugin.
+
+Usage
+------------
+
+The plugin provides an admin interface for ExportCardConfiguration CRUD. Existing ExportCardConfigurations can then be used to export data in PDF form, the configuration defining the layout of the card and the specific data which appears in it. The DocumentGenerator init takes a ExportCardConfiguration and an array of any object. It is left to the developer to make sure the fields in the config match the given data. A ExportCardConfiguration currently allows for the following fields to be defined:
+
+Name - A unique identifier for the configuration.
+Per Page - The number of export cards which will appear on each page of the exported PDF.
+Page Size - Currently we only support A4 paper size.
+Orientation - Portrait of Landscape.
+Rows - A YAML text block which defines in detail what should appear in each row and column of the export cards.
+
+The following sample YAML shows the required form and all of the available configuration options:
+
+
+group1:
+ has_border: false
+ height: 200
+ rows:
+ row1:
+ height: 50
+ priority: 1
+ columns:
+ id:
+ has_label: false
+ min_font_size: 10
+ max_font_size: 20
+ font_size: 20
+ font_style: bold
+ text_align: left
+ minimum_lines: 2
+ render_if_empty: false
+ width: 30%
+ due_date:
+ has_label: false
+ font_size: 15
+ font_style: italic
+ minimum_lines: 2
+ render_if_empty: false
+ width: 70%
+ row2:
+ priority: 2
+ columns:
+ status:
+ has_label: true
+ indented: true
+ font_size: 15
+ font_style: normal
+ minimum_lines: 1
+ render_if_empty: true
+group2:
+ has_border: true
+ rows:
+ row1:
+ height: 80
+ priority: 2
+ columns:
+ description:
+ has_label: true
+ indented: false
+ font_size: 15
+ font_style: normal
+ minimum_lines: 1
+ render_if_empty: true
+ row2:
+ priority: 2
+ columns:
+ status:
+ has_label: true
+ font_size: 15
+ font_style: normal
+ minimum_lines: 1
+ render_if_empty: true
+ row2:
+ priority: 2
+ columns:
+ custom_field_name:
+ has_label: true
+ font_size: 15
+ minimum_lines: 1
+group3:
+ rows:
+ row1:
+ priority: 2
+ columns:
+ children:
+ has_label: true
+ has_count: true
+ indented: true
+ font_size: 15
+ font_style: normal
+ minimum_lines: 1
+ render_if_empty: true
+
+
+The config is divided into groups. A group can have a height property which will enforce the minimum height of the group in pixels. The has_border property can be set to true which will draw a border around the rows in the group.
+
+Any number of rows can be defined. The font_size and minimum_lines properties define how much height on the card is given to the row. The plugin will attempt to assign enough space to each of the rows, however space will be assigned based on the priorities of the the rows, with rows with lower priority (higher numbers) being reduced and removed first if there is not enough for all the data. The row height can be forced by giving a value, in pixels, for the row height property. This will override the assigned row height.
+
+The name of the column informs the plugin which data should be read from the model (status, due_date, id, etc.). There can be any number of columns per row. Custom field names can also be used. Columns are given an equal share of the row width unless a specific width % is given. If there is more text in the column than can fit into its assinged space on the card then the text will be truncated.
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/pdf-export
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/openproject-pdf_export`
+
+
+Credits
+-------
+
+We would like to thank
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorhip
+
+Licence
+-------
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-pdf_export/app/controllers/export_card_configurations_controller.rb b/vendored-plugins/openproject-pdf_export/app/controllers/export_card_configurations_controller.rb
new file mode 100644
index 0000000000..aca62d04c9
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/controllers/export_card_configurations_controller.rb
@@ -0,0 +1,113 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+class ExportCardConfigurationsController < ApplicationController
+ layout 'admin'
+
+ before_filter :require_admin
+ before_filter :load_config, only: [:show, :update, :edit, :destroy, :activate, :deactivate]
+ before_filter :load_configs, only: [:index]
+
+ def index
+ end
+
+ def show
+ end
+
+ def edit
+ end
+
+ def new
+ @config = ExportCardConfiguration.new
+ end
+
+ def create
+ @config = ExportCardConfiguration.new(export_card_configurations_params)
+ if @config.save
+ flash[:notice] = l(:notice_successful_create)
+ redirect_to :action => 'index'
+ else
+ render "new"
+ end
+ end
+
+ def update
+ if cannot_update_default
+ flash[:error] = l(:error_can_not_change_name_of_default_configuration)
+ render "edit"
+ elsif @config.update_attributes(export_card_configurations_params)
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'index'
+ else
+ render "edit"
+ end
+ end
+
+ def destroy
+ if !@config.is_default? && @config.destroy
+ flash[:notice] = l(:notice_successful_delete)
+ else
+ flash[:notice] = l(:error_can_not_delete_export_card_configuration)
+ end
+ redirect_to :action => 'index'
+ end
+
+ def activate
+ if @config.activate
+ flash[:notice] = l(:notice_export_card_configuration_activated)
+ else
+ flash[:notice] = l(:error_can_not_activate_export_card_configuration)
+ end
+ redirect_to :action => 'index'
+ end
+
+ def deactivate
+ if @config.deactivate
+ flash[:notice] = l(:notice_export_card_configuration_deactivated)
+ else
+ flash[:notice] = l(:error_can_not_deactivate_export_card_configuration)
+ end
+ redirect_to :action => 'index'
+ end
+
+ private
+
+ def cannot_update_default
+ @config.is_default? && export_card_configurations_params[:name].downcase != "default"
+ end
+
+ def export_card_configurations_params
+ params.require(:export_card_configuration).permit(:name, :rows, :per_page, :page_size, :orientation, :description)
+ end
+
+ def load_config
+ @config = ExportCardConfiguration.find(params[:id])
+ end
+
+ def load_configs
+ @configs = ExportCardConfiguration.all
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/app/models/export_card_configuration.rb b/vendored-plugins/openproject-pdf_export/app/models/export_card_configuration.rb
new file mode 100644
index 0000000000..4e28177ad4
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/models/export_card_configuration.rb
@@ -0,0 +1,151 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+class ExportCardConfiguration < ActiveRecord::Base
+
+ class RowsYamlValidator < ActiveModel::Validator
+ REQUIRED_GROUP_KEYS = ["rows"]
+ VALID_GROUP_KEYS = ["rows", "has_border", "height"]
+ REQUIRED_ROW_KEYS = ["columns"]
+ VALID_ROW_KEYS = ["columns", "height", "priority"]
+ # TODO: Security Consideration
+ # Should we define which model properties are visible and if so how?
+ # VALID_MODEL_PROPERTIES = [""]
+ REQUIRED_COLUMN_KEYS = []
+ VALID_COLUMN_KEYS = ["has_label", "min_font_size", "max_font_size",
+ "font_size", "font_style", "text_align", "minimum_lines", "render_if_empty",
+ "width", "indented", "custom_label", "has_count"]
+ NUMERIC_COLUMN_VALUE = ["min_font_size", "max_font_size", "font_size", "minimum_lines"]
+
+ def raise_yaml_error
+ raise ArgumentError, I18n.t('validation_error_yaml_is_badly_formed')
+ end
+
+ def assert_required_keys(hash, valid_keys, required_keys)
+ raise_yaml_error if !hash.is_a?(Hash)
+
+ begin
+ hash.assert_valid_keys valid_keys
+ rescue ArgumentError => e
+ # Small hack alert: Catch a raise error again but with localised text
+ raise ArgumentError, "#{I18n.t('validation_error_uknown_key')} '#{e.message.split(": ")[1]}'"
+ end
+
+ pending_keys = required_keys - hash.keys
+ raise(ArgumentError, "#{I18n.t('validation_error_required_keys_not_present')} #{pending_keys.join(", ")}") unless pending_keys.empty?
+ end
+
+ def check_valid_value_type(value, type)
+ raise(ArgumentError, "#{I18n.t('validation_error_yaml_is_badly_formed')}") unless value.is_a?type
+ end
+
+ def validate(record)
+ begin
+ if record.rows.nil? || !(YAML::load(record.rows)).is_a?(Hash)
+ record.errors[:rows] << I18n.t('validation_error_yaml_is_badly_formed')
+ return false
+ end
+ rescue Psych::SyntaxError => e
+ record.errors[:rows] << I18n.t('validation_error_yaml_is_badly_formed')
+ return false
+ end
+
+ begin
+ groups = YAML::load(record.rows)
+ groups.each do |gk, gv|
+ assert_required_keys(gv, VALID_GROUP_KEYS, REQUIRED_GROUP_KEYS)
+ raise_yaml_error if !gv["rows"].is_a?(Hash)
+ gv["rows"].each do |rk, rv|
+ assert_required_keys(rv, VALID_ROW_KEYS, REQUIRED_ROW_KEYS)
+ raise_yaml_error if !rv["columns"].is_a?(Hash)
+ rv["columns"].each do |ck, cv|
+ assert_required_keys(cv, VALID_COLUMN_KEYS, REQUIRED_COLUMN_KEYS)
+ cv.map{|cname, cvalue | check_valid_value_type(cvalue, Numeric) if NUMERIC_COLUMN_VALUE.include?(cname)}
+ end
+ end
+ end
+ rescue ArgumentError => e
+ record.errors[:rows] << "#{I18n.t('yaml_error')} #{e.message}"
+ end
+ end
+ end
+
+ include OpenProject::PdfExport::Exceptions
+
+ validates :name, presence: true
+ validates :rows, rows_yaml: true
+ validates :per_page, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
+ validates :page_size, inclusion: { in: %w(A4) }, allow_nil: false
+ validates :orientation, inclusion: { in: %w(landscape portrait) }, allow_nil: true
+
+ scope :active, -> { where(active: true) }
+
+ def self.default
+ ExportCardConfiguration.active.select { |c| c.is_default? }.first || ExportCardConfiguration.active.first
+ end
+
+ def activate
+ self.update_attributes!({active: true})
+ end
+
+ def deactivate
+ if !self.is_default?
+ self.update_attributes!({active: false})
+ else
+ false
+ end
+ end
+
+ def landscape?
+ !portrait?
+ end
+
+ def portrait?
+ orientation == "portrait"
+ end
+
+ def rows_hash
+ config = YAML::load(rows)
+ raise BadlyFormedExportCardConfigurationError.new(I18n.t('validation_error_yaml_is_badly_formed')) if !config.is_a?(Hash)
+ config
+ end
+
+ def is_default?
+ self.name.downcase == "default"
+ end
+
+ def can_delete?
+ !self.is_default?
+ end
+
+ def can_activate?
+ !self.active
+ end
+
+ def can_deactivate?
+ self.active && !is_default?
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_form.html.erb b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_form.html.erb
new file mode 100644
index 0000000000..9ec5ac66d2
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_form.html.erb
@@ -0,0 +1,42 @@
+<%#-- copyright
+OpenProject PDF Export Plugin
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+
+ <%= f.text_field :name, required: true %>
+
+
+ <%= f.text_area :description %>
+
+
+ <%= f.number_field :per_page, required: true, label: I18n.t('export_config_per_page') %>
+
+
+ <%= f.text_field :page_size, required: true, value: "A4", readonly: true, label: I18n.t('export_config_page_size') %>
+
+
+ <%= f.select :orientation, [:landscape, :portrait], :required => true, :label => I18n.t('export_config_orientation') %>
+
+<%= render partial: "rows_format_help", locals: { f: f } %>
diff --git a/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_rows_format_help.html.erb b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_rows_format_help.html.erb
new file mode 100644
index 0000000000..58d06b3c97
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/_rows_format_help.html.erb
@@ -0,0 +1,10 @@
+
+ <%= f.text_area :rows, required: true, label: I18n.t('export_config_rows') %>
+
+
diff --git a/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/edit.html.erb b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/edit.html.erb
new file mode 100644
index 0000000000..2144037ee8
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/edit.html.erb
@@ -0,0 +1,38 @@
+<%#-- copyright
+OpenProject PDF Export Plugin
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_administration), "#{l(:label_edit)} #{l(:label_export_card_configuration)} #{@config.name}" %>
+
+<%= error_messages_for 'config' %>
+
+<%= link_to t(:label_export_card_configuration_plural), pdf_export_export_card_configurations_path %> » <%= h(@config.name) %>
+
+<%= labelled_tabular_form_for @config, url: pdf_export_export_card_configuration_path(@config) do |f| -%>
+ <%= render partial: 'form', locals: { f: f } %>
+
+ <%= styled_button_tag l(:button_save), class: "-highlight -with-icon icon-checkmark" %>
+<% end %>
+
diff --git a/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/index.html.erb b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/index.html.erb
new file mode 100644
index 0000000000..be3b23b287
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/index.html.erb
@@ -0,0 +1,132 @@
+<%#-- copyright
+OpenProject PDF Export Plugin
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_administration), l(:label_export_card_configuration_plural) %>
+
+<%=l(:label_export_card_configuration_plural)%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% for config in @configs %>
+
+ <%= link_to config.name, edit_pdf_export_export_card_configuration_path(config) %>
+ <%= config.page_size %>
+ <%= config.per_page %>
+ <%= config.orientation %>
+ <%= config.active %>
+
+ <% if config.can_delete? %>
+ <%= link_to pdf_export_export_card_configuration_path(config), :method => :delete, :confirm => t(:text_are_you_sure), :class => 'icon-context icon-delete' do %>
+ <%= t(:button_delete) %>
+ <%=h config.name %>
+ <% end %>
+ <% end %>
+ <% if config.can_activate? %>
+ <%= link_to activate_pdf_export_export_card_configuration_path(config), :method => :post, :class => 'icon-context icon-delete' do %>
+ <%= t(:label_export_card_activate) %>
+ <%=h config.name %>
+ <% end %>
+ <% end %>
+ <% if config.can_deactivate? %>
+ <%= link_to deactivate_pdf_export_export_card_configuration_path(config), :method => :post, :class => 'icon-context icon-delete' do %>
+ <%= t(:label_export_card_deactivate) %>
+ <%=h config.name %>
+ <% end %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+
+ <%= link_to({action: 'new'}, class: 'button -alt-highlight') do %>
+
+ <%= l(:label_export_card_configuration_new) %>
+ <% end %>
+
diff --git a/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/new.html.erb b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/new.html.erb
new file mode 100644
index 0000000000..8aae901780
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/app/views/export_card_configurations/new.html.erb
@@ -0,0 +1,37 @@
+<%#-- copyright
+OpenProject PDF Export Plugin
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
+
+++#%>
+
+<% html_title l(:label_administration), l(:label_export_card_configuration_new) %>
+
+<%= error_messages_for 'config' %>
+
+<%= link_to t(:label_export_card_configuration_plural), pdf_export_export_card_configurations_path %> » <%= t(:label_export_card_configuration_new) %>
+
+<%= labelled_tabular_form_for @config, url: pdf_export_export_card_configurations_path do |f| -%>
+ <%= render partial: 'form', locals: { f: f } %>
+
+ <%= styled_button_tag l(:button_create), class: "-highlight -with-icon icon-checkmark" %>
+<% end %>
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/da.yml b/vendored-plugins/openproject-pdf_export/config/locales/da.yml
new file mode 100644
index 0000000000..e5af6688a9
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/da.yml
@@ -0,0 +1,30 @@
+da:
+ error_can_not_delete_export_card_configuration: Denne opsætning kan ikke slettes.
+ error_can_not_change_name_of_default_configuration: Navnet på standardopsætningen kan ikke ændres.
+ label_backlogs_export_card_config_select: Vælg eksportkortopsætning
+ label_backlogs_export_card_export: Eksportér
+ label_export_card_configuration_new: Ny eksportkortopsætning
+ label_export_card_configuration: Eksportkortopsætning
+ label_export_card_configuration_plural: Eksportkortopsætninger
+ label_export_card_activate: Aktivér
+ label_export_card_deactivate: Deaktivér
+ notice_export_card_configuration_activated: Opsætning aktiveret
+ notice_export_card_configuration_deactivated: Opsætning deaktiveret
+ error_can_not_activate_export_card_configuration: Denne opsætning kan ikke aktiveres
+ error_can_not_deactivate_export_card_configuration: Denne opsætning kan ikke deaktiveres
+ validation_error_required_keys_not_present: 'Påkrævede nøgle(-r) ikke til stede:'
+ validation_error_yaml_is_badly_formed: har intet gyldigt YAML-format.
+ validation_error_uknown_key: 'Ukendt nøgle:'
+ yaml_error: 'YAML-fejl:'
+ help_link_rows_format: Rækkeformatering
+ export_config_per_page: Pr. side
+ export_config_page_size: Sidestørrelse
+ export_config_orientation: Retning
+ export_config_rows: Rækker
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Rækker
+ per_page: Pr. side
+ page_size: Sidestørrelse
+ orientation: Retning
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/de.yml b/vendored-plugins/openproject-pdf_export/config/locales/de.yml
new file mode 100644
index 0000000000..4618eb0a72
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/de.yml
@@ -0,0 +1,30 @@
+de:
+ error_can_not_delete_export_card_configuration: Diese Konfiguration kann nicht gelöscht werden.
+ error_can_not_change_name_of_default_configuration: Der Name der Standardkonfiguration kann nicht geändert werden.
+ label_backlogs_export_card_config_select: Bitte wählen Sie eine Export-Kartenkonfiguration
+ label_backlogs_export_card_export: Exportieren
+ label_export_card_configuration_new: Neue Export-Kartenkonfiguration
+ label_export_card_configuration: Export-Kartenkonfiguration
+ label_export_card_configuration_plural: Export-Kartenkonfigurationen
+ label_export_card_activate: Aktivieren
+ label_export_card_deactivate: De-aktivieren
+ notice_export_card_configuration_activated: Export-Kartenkonfiguration erfolgreich aktiviert
+ notice_export_card_configuration_deactivated: Export-Kartenkonfiguration erfolgreich de-aktiviert
+ error_can_not_activate_export_card_configuration: Diese Konfiguration kann nicht aktiviert werden
+ error_can_not_deactivate_export_card_configuration: Diese Konfiguration kann nicht de-aktiviert werden
+ validation_error_required_keys_not_present: 'Erfordete(r) Schlüssel nicht vorhanden:'
+ validation_error_yaml_is_badly_formed: hat kein gültiges YAML-Format.
+ validation_error_uknown_key: 'Unbekannter Schlüssel:'
+ yaml_error: 'YAML Fehler:'
+ help_link_rows_format: Reihen formatieren
+ export_config_per_page: Pro Seite
+ export_config_page_size: Seitengröße
+ export_config_orientation: Ausrichtung
+ export_config_rows: Reihen
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Reihen
+ per_page: Pro Seite
+ page_size: Seitengröße
+ orientation: Ausrichtung
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/en.yml b/vendored-plugins/openproject-pdf_export/config/locales/en.yml
new file mode 100644
index 0000000000..ee9a8cf805
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/en.yml
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+en:
+ error_can_not_delete_export_card_configuration: "This config cannot be deleted."
+ error_can_not_change_name_of_default_configuration: "The name of the default config cannot be changed."
+ label_backlogs_export_card_config_select: "Select export card configuration"
+ label_backlogs_export_card_export: "Export"
+ label_export_card_configuration_new: "New Export Card Config"
+ label_export_card_configuration: "Export Card Config"
+ label_export_card_configuration_plural: "Export Card Configs"
+ label_export_card_activate: "Activate"
+ label_export_card_deactivate: "De-activate"
+ notice_export_card_configuration_activated: "Config succesfully activated"
+ notice_export_card_configuration_deactivated: "Config succesfully de-activated"
+ error_can_not_activate_export_card_configuration: "This config cannot be activated"
+ error_can_not_deactivate_export_card_configuration: "This config cannot be de-activated"
+ validation_error_required_keys_not_present: "Required key(s) not present:"
+ validation_error_yaml_is_badly_formed: "has no valid YAML format."
+ validation_error_uknown_key: "Unknown key:"
+ yaml_error: "YAML error:"
+ help_link_rows_format: "Rows Formatting"
+ export_config_per_page: "Per page"
+ export_config_page_size: "Page size"
+ export_config_orientation: "Orientation"
+ export_config_rows: "Rows"
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: "Rows"
+ per_page: "Per page"
+ page_size: "Page size"
+ orientation: "Orientation"
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/fr.yml b/vendored-plugins/openproject-pdf_export/config/locales/fr.yml
new file mode 100644
index 0000000000..0e75fd911a
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/fr.yml
@@ -0,0 +1,30 @@
+fr:
+ error_can_not_delete_export_card_configuration: Cette configuration ne peut pas être supprimée.
+ error_can_not_change_name_of_default_configuration: Le nom de la configuration par défaut ne peut pas être modifié.
+ label_backlogs_export_card_config_select: "Sélectionnez la configuration de la carte d'exportation"
+ label_backlogs_export_card_export: Exporter
+ label_export_card_configuration_new: "Nouvelle configuration de la carte d'exportation"
+ label_export_card_configuration: "Configuration de la carte d'exportation"
+ label_export_card_configuration_plural: "Configuration des cartes d'exportation"
+ label_export_card_activate: Activer
+ label_export_card_deactivate: Désactiver
+ notice_export_card_configuration_activated: Configuration activée avec succès
+ notice_export_card_configuration_deactivated: Configuration désactivée avec succès
+ error_can_not_activate_export_card_configuration: Cette configuration ne peut pas être activée
+ error_can_not_deactivate_export_card_configuration: Cette configuration ne peut pas être désactivée
+ validation_error_required_keys_not_present: 'La/Les clé(s) requise(nt) est/sont abente(s) :'
+ validation_error_yaml_is_badly_formed: has no valid YAML format.
+ validation_error_uknown_key: 'Clé inconnue :'
+ yaml_error: 'Erreur YAML :'
+ help_link_rows_format: Mise en forme des lignes
+ export_config_per_page: Par page
+ export_config_page_size: Taille de la page
+ export_config_orientation: Orientation
+ export_config_rows: Lignes
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Lignes
+ per_page: Par page
+ page_size: Taille de la page
+ orientation: Orientation
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/it.yml b/vendored-plugins/openproject-pdf_export/config/locales/it.yml
new file mode 100644
index 0000000000..8690e98d58
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/it.yml
@@ -0,0 +1,30 @@
+it:
+ error_can_not_delete_export_card_configuration: Questa configurazione non può essere eliminata.
+ error_can_not_change_name_of_default_configuration: Il nome della configurazione predefinita non può essere modificato.
+ label_backlogs_export_card_config_select: Selezionare esporta configurazione della scheda
+ label_backlogs_export_card_export: Esporta
+ label_export_card_configuration_new: Nuova esportazione scheda Config
+ label_export_card_configuration: Esporta la configurazione della scheda
+ label_export_card_configuration_plural: Esporta le configurazioni della scheda
+ label_export_card_activate: Attivare
+ label_export_card_deactivate: Disattivare
+ notice_export_card_configuration_activated: Configurazione attivata con successo
+ notice_export_card_configuration_deactivated: Configurazione disattivata con successo
+ error_can_not_activate_export_card_configuration: La configurazione non può essere attivata
+ error_can_not_deactivate_export_card_configuration: La configurazione non può essere disattivata
+ validation_error_required_keys_not_present: 'Chiave(i) richiesta non presente:'
+ validation_error_yaml_is_badly_formed: non ha alcun valido formato YAML.
+ validation_error_uknown_key: 'Chiave sconosciuta:'
+ yaml_error: 'Errore YAML:'
+ help_link_rows_format: Formattazione di righe
+ export_config_per_page: Per pagina
+ export_config_page_size: Dimensioni della pagina
+ export_config_orientation: Orientamento
+ export_config_rows: Righe
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Righe
+ per_page: Per pagina
+ page_size: Dimensioni della pagina
+ orientation: Orientamento
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/pt-BR.yml b/vendored-plugins/openproject-pdf_export/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..54447643c9
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/pt-BR.yml
@@ -0,0 +1,30 @@
+pt-BR:
+ error_can_not_delete_export_card_configuration: Esta configuração não pode ser excluída.
+ error_can_not_change_name_of_default_configuration: O nome da configuração padrão não pode ser alterado.
+ label_backlogs_export_card_config_select: Selecione configuração de exportação de cartão
+ label_backlogs_export_card_export: Exportar
+ label_export_card_configuration_new: Nova configuração de exportação de cartões
+ label_export_card_configuration: Configuração de exportação de cartões
+ label_export_card_configuration_plural: Configurações de exportação de cartões
+ label_export_card_activate: Ativar
+ label_export_card_deactivate: Desativar
+ notice_export_card_configuration_activated: Configuração ativada com sucesso
+ notice_export_card_configuration_deactivated: Configuração desativada com sucesso
+ error_can_not_activate_export_card_configuration: Esta configuração não pode ser ativada
+ error_can_not_deactivate_export_card_configuration: Esta configuração não pode ser desativada
+ validation_error_required_keys_not_present: 'Chave(s) necessária(s) não está(ão) presente(s):'
+ validation_error_yaml_is_badly_formed: não há nenhum formato YAML válido.
+ validation_error_uknown_key: 'Chave desconhecida:'
+ yaml_error: 'Erro YAML:'
+ help_link_rows_format: Formatação das linhas
+ export_config_per_page: Por página
+ export_config_page_size: Tamanho da página
+ export_config_orientation: Orientação
+ export_config_rows: Linhas
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Linhas
+ per_page: Por página
+ page_size: Tamanho da página
+ orientation: Orientação
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/ru.yml b/vendored-plugins/openproject-pdf_export/config/locales/ru.yml
new file mode 100644
index 0000000000..18bb93648a
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/ru.yml
@@ -0,0 +1,30 @@
+ru:
+ error_can_not_delete_export_card_configuration: Невозможно удалить эту конфигурацию.
+ error_can_not_change_name_of_default_configuration: Имя конфигурации по умолчанию не может быть изменено.
+ label_backlogs_export_card_config_select: Выберите экспорт карты конфигурации
+ label_backlogs_export_card_export: Экспорт
+ label_export_card_configuration_new: Новый экспорт карт конфигурации
+ label_export_card_configuration: Экспорт карты конфигурации
+ label_export_card_configuration_plural: Экспорт карт конфигураций
+ label_export_card_activate: Активировать
+ label_export_card_deactivate: Деактивировать
+ notice_export_card_configuration_activated: Конфигурация успешно активирована
+ notice_export_card_configuration_deactivated: Конфигурация успешно деактивирована
+ error_can_not_activate_export_card_configuration: Эта конфигурация не может быть активирована
+ error_can_not_deactivate_export_card_configuration: Эта конфигурация не может быть деактивирована
+ validation_error_required_keys_not_present: 'Требуемые ключи не предъявлены:'
+ validation_error_yaml_is_badly_formed: имеет не действительный формат YAML.
+ validation_error_uknown_key: 'Неизвестный ключ:'
+ yaml_error: 'Ошибка YAML:'
+ help_link_rows_format: Форматирование строк
+ export_config_per_page: Постранично
+ export_config_page_size: Размер страницы
+ export_config_orientation: Ориентация
+ export_config_rows: Строк
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Строк
+ per_page: Постранично
+ page_size: Размер страницы
+ orientation: Ориентация
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/sk.yml b/vendored-plugins/openproject-pdf_export/config/locales/sk.yml
new file mode 100644
index 0000000000..12f128fe8e
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/sk.yml
@@ -0,0 +1,30 @@
+sk:
+ error_can_not_delete_export_card_configuration: Táto konfigurácia nemôže byť odstránená.
+ error_can_not_change_name_of_default_configuration: Názov predvolenej konfigurácie nie je možné zmeniť.
+ label_backlogs_export_card_config_select: Zvoľte Konfiguráciu export karty
+ label_backlogs_export_card_export: Exportovať
+ label_export_card_configuration_new: Nová konfigurácia export karty
+ label_export_card_configuration: Konfigurácia export karty
+ label_export_card_configuration_plural: Konfigurácie export karty
+ label_export_card_activate: Aktivovať
+ label_export_card_deactivate: Deaktivovať
+ notice_export_card_configuration_activated: Konfigurácia úspešne aktivovaná
+ notice_export_card_configuration_deactivated: Konfigurácia úspešne deaktivovaná
+ error_can_not_activate_export_card_configuration: Túto konfiguráciu nie je možné aktivovať
+ error_can_not_deactivate_export_card_configuration: Túto konfiguráciu nie je možné deaktivovať
+ validation_error_required_keys_not_present: 'Požadovaný kľúč (kľúče) nenájdené:'
+ validation_error_yaml_is_badly_formed: obsahuje neplatné YAML formátovanie.
+ validation_error_uknown_key: 'Neznámy kľúč:'
+ yaml_error: 'YAML chyba:'
+ help_link_rows_format: Formátovanie riadkov
+ export_config_per_page: Na stránku
+ export_config_page_size: Veľkosť stránky
+ export_config_orientation: Orientácia
+ export_config_rows: Riadky
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Riadky
+ per_page: Na stránku
+ page_size: Veľkosť stránky
+ orientation: Orientácia
diff --git a/vendored-plugins/openproject-pdf_export/config/locales/sv-SE.yml b/vendored-plugins/openproject-pdf_export/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..9101b29175
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/locales/sv-SE.yml
@@ -0,0 +1,30 @@
+sv:
+ error_can_not_delete_export_card_configuration: Denna konfiguration kan inte tas bort.
+ error_can_not_change_name_of_default_configuration: Namnet på standardkonfigurationen kan inte ändras.
+ label_backlogs_export_card_config_select: Välj Export-kort konfiguration
+ label_backlogs_export_card_export: Exportera
+ label_export_card_configuration_new: Ny Export-kort konfiguration
+ label_export_card_configuration: Export-kort konfiguration
+ label_export_card_configuration_plural: Export-kort konfigurationer
+ label_export_card_activate: Aktivera
+ label_export_card_deactivate: De-aktivera
+ notice_export_card_configuration_activated: Konfigurationen aktiverad
+ notice_export_card_configuration_deactivated: Konfigurationen deaktiverad
+ error_can_not_activate_export_card_configuration: Denna konfiguration kan inte aktiveras
+ error_can_not_deactivate_export_card_configuration: Denna konfiguration kan inte deaktiveras
+ validation_error_required_keys_not_present: 'Krävda nycklar är inte tillgängliga:'
+ validation_error_yaml_is_badly_formed: has no valid YAML format.
+ validation_error_uknown_key: 'Okänd nyckel:'
+ yaml_error: 'YAML fel:'
+ help_link_rows_format: Radformatering
+ export_config_per_page: Per sida
+ export_config_page_size: Sidstorlek
+ export_config_orientation: Orientering
+ export_config_rows: Rader
+ activerecord:
+ attributes:
+ export_card_configuration:
+ rows: Rader
+ per_page: Per sida
+ page_size: Sidstorlek
+ orientation: Orientering
diff --git a/vendored-plugins/openproject-pdf_export/config/routes.rb b/vendored-plugins/openproject-pdf_export/config/routes.rb
new file mode 100644
index 0000000000..7704d0b978
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/config/routes.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+OpenProject::Application.routes.draw do
+
+ scope "", as: "pdf_export" do
+ resources :export_card_configurations, :controller => :export_card_configurations do
+ post 'activate', on: :member
+ post 'deactivate', on: :member
+ end
+ end
+
+end
diff --git a/vendored-plugins/openproject-pdf_export/db/migrate/20140113132617_create_export_card_configuration.rb b/vendored-plugins/openproject-pdf_export/db/migrate/20140113132617_create_export_card_configuration.rb
new file mode 100644
index 0000000000..a55dc7acc2
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/db/migrate/20140113132617_create_export_card_configuration.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class CreateExportCardConfiguration < ActiveRecord::Migration
+ def change
+ create_table :export_card_configurations do |t|
+ t.string :name
+ t.integer :per_page
+ t.string :page_size
+ t.string :orientation
+ t.text :rows
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/db/migrate/20140129103924_add_active_to_export_card_configurations.rb b/vendored-plugins/openproject-pdf_export/db/migrate/20140129103924_add_active_to_export_card_configurations.rb
new file mode 100644
index 0000000000..c6b2a052ed
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/db/migrate/20140129103924_add_active_to_export_card_configurations.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class AddActiveToExportCardConfigurations < ActiveRecord::Migration
+ def change
+ add_column :export_card_configurations, :active, :boolean, default: true
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/db/migrate/20140207134248_add_description_to_export_card_configurations.rb b/vendored-plugins/openproject-pdf_export/db/migrate/20140207134248_add_description_to_export_card_configurations.rb
new file mode 100644
index 0000000000..2053b741fc
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/db/migrate/20140207134248_add_description_to_export_card_configurations.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+class AddDescriptionToExportCardConfigurations < ActiveRecord::Migration
+ def change
+ add_column :export_card_configurations, :description, :text
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/db/seeds/export_card_configurations.rb b/vendored-plugins/openproject-pdf_export/db/seeds/export_card_configurations.rb
new file mode 100644
index 0000000000..98f260d16b
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/db/seeds/export_card_configurations.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+if ExportCardConfiguration.find_by_identifier("default").nil?
+ ExportCardConfiguration.create({name: "Default",
+ per_page: 2,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "rows:\n row1:\n has_border: false\n columns:\n id:\n has_label: false\n font_size: 20\n font_style: bold\n priority: 1\n minimum_lines: 2\n render_if_empty: false\n width: 30%\n due_date:\n has_label: false\n font_size: 15\n font_style: italic\n priority: 1\n minimum_lines: 2\n render_if_empty: false\n width: 70%\n row2:\n has_border: false\n columns:\n description:\n has_label: false\n font_size: 15\n font_style: normal\n priority: 4\n minimum_lines: 5\n render_if_empty: false\n width: 100%\n"})
+end
diff --git a/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT.md b/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..da8af467bf
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject PDF Export Plugin
+
+This Plugin adds features which enable the configuration and generation of printable export card PDFs.
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
\ No newline at end of file
diff --git a/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..3a4f9bca5a
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/doc/COPYRIGHT_short.md
@@ -0,0 +1,22 @@
+OpenProject PDF Export Plugin
+
+Copyright (C)2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License version 3.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
diff --git a/vendored-plugins/openproject-pdf_export/doc/GPL.txt b/vendored-plugins/openproject-pdf_export/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-pdf_export/features/export_card_configurations_admin.feature b/vendored-plugins/openproject-pdf_export/features/export_card_configurations_admin.feature
new file mode 100644
index 0000000000..df04d68acf
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/features/export_card_configurations_admin.feature
@@ -0,0 +1,80 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+Feature: export card configurations Admin
+ As an Admin
+ I want to administer the export card configurations
+ So that CRUD operations can be performed on them
+
+ @javascript
+ Scenario: View Configurations
+ Given there are multiple export card configurations
+ And I am admin
+ And I am on the export card configurations index page
+ Then I should see "Default"
+ And I should see "Custom"
+ And I should see "Custom 2"
+
+ @javascript
+ Scenario: Create New Configuration
+ Given there are multiple export card configurations
+ And I am admin
+ And I am on the export card configurations index page
+ When I follow "New Export Card Config"
+ And I fill in "Config 1" for "export_card_configuration_name"
+ And I fill in "5" for "export_card_configuration_per_page"
+ And I select "landscape" from "export_card_configuration_orientation"
+ And I fill in valid YAML for export config rows
+ And I submit the form by the "Create" button
+ Then I should see "Successful creation." within ".flash.notice"
+
+ @javascript
+ Scenario: Edit Existing Configuration
+ Given there are multiple export card configurations
+ And I am admin
+ And I am on the export card configurations index page
+ When I follow first "Custom 2"
+ And I fill in "5" for "export_card_configuration_per_page"
+ And I select "portrait" from "export_card_configuration_orientation"
+ And I fill in valid YAML for export config rows
+ And I submit the form by the "Save" button
+ Then I should see "Successful update." within ".flash.notice"
+
+ @javascript
+ Scenario: Activate Existing Configuration
+ Given there are multiple export card configurations
+ And I am admin
+ And I am on the export card configurations index page
+ When I follow first "Activate"
+ Then I should see "Config succesfully activated" within ".flash.notice"
+
+ @javascript
+ Scenario: Deactivate Existing Configuration
+ Given there are multiple export card configurations
+ And I am admin
+ And I am on the export card configurations index page
+ When I follow first "De-activate"
+ Then I should see "Config succesfully de-activated" within ".flash.notice"
\ No newline at end of file
diff --git a/vendored-plugins/openproject-pdf_export/features/step_definitions/given.rb b/vendored-plugins/openproject-pdf_export/features/step_definitions/given.rb
new file mode 100644
index 0000000000..0c2fac0102
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/features/step_definitions/given.rb
@@ -0,0 +1,82 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+Given /^there are multiple export card configurations$/ do
+ config1 = ExportCardConfiguration.create!({
+ name: "Default",
+ description: "This is a description",
+ active: true,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ })
+ config2 = ExportCardConfiguration.create!({
+ name: "Custom",
+ description: "This is a description",
+ active: true,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ })
+ config3 = ExportCardConfiguration.create!({
+ name: "Custom 2",
+ description: "This is a description",
+ active: true,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ })
+ config4 = ExportCardConfiguration.create!({
+ name: "Custom Inactive",
+ description: "This is a description",
+ active: false,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ })
+ [config1, config2, config3, config4]
+end
+
+Given /^there is the default export card configuration$/ do
+ config1 = ExportCardConfiguration.create!({
+ name: "Default",
+ description: "This is a description",
+ active: true,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ })
+ [config1]
+end
+
+Given /^I fill in valid YAML for export config rows$/ do
+ valid_yaml = "groups:\n rows:\n row1:\n columns:\n id:\n has_label: false"
+ fill_in("export_card_configuration_rows", :with => valid_yaml)
+end
diff --git a/vendored-plugins/openproject-pdf_export/features/step_definitions/then.rb b/vendored-plugins/openproject-pdf_export/features/step_definitions/then.rb
new file mode 100644
index 0000000000..b988c92d5b
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/features/step_definitions/then.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
diff --git a/vendored-plugins/openproject-pdf_export/features/step_definitions/when.rb b/vendored-plugins/openproject-pdf_export/features/step_definitions/when.rb
new file mode 100644
index 0000000000..d03419168b
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/features/step_definitions/when.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+When /^(?:|I )follow first "([^"]*)"$/ do |link|
+ first(:link, link).click
+end
diff --git a/vendored-plugins/openproject-pdf_export/features/support/paths.rb b/vendored-plugins/openproject-pdf_export/features/support/paths.rb
new file mode 100644
index 0000000000..ef5ae688eb
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/features/support/paths.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+module PdfExportNavigationHelpers
+ # Maps a name to a path. Used by the
+ #
+ # When /^I go to (.+)$/ do |page_name|
+ #
+ # step definition in web_steps.rb
+ #
+ def path_to(page_name)
+ case page_name
+ when /^the export card configurations index page$/
+ "/export_card_configurations"
+
+ else
+ super
+ end
+ end
+end
+
+World(PdfExportNavigationHelpers)
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export.rb
new file mode 100644
index 0000000000..a2b5075d8d
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module PdfExport
+ require "open_project/pdf_export/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/engine.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/engine.rb
new file mode 100644
index 0000000000..c19c309872
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/engine.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/plugins'
+
+module OpenProject::PdfExport
+ class Engine < ::Rails::Engine
+ engine_name :openproject_pdf_export
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-pdf_export',
+ :author_url => 'http://finn.de',
+ :requires_openproject => '>= 4.0.0' do
+
+ menu :admin_menu,
+ :export_card_configurations,
+ {:controller => '/export_card_configurations', :action => 'index'},
+ {:caption => :'label_export_card_configuration_plural', :html => {:class => "icon2 icon-ticket-down"}}
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/exceptions.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/exceptions.rb
new file mode 100644
index 0000000000..1b29363b00
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/exceptions.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::PdfExport::Exceptions
+ class BadlyFormedExportCardConfigurationError < StandardError
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/card_element.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/card_element.rb
new file mode 100644
index 0000000000..1b6a02f1db
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/card_element.rb
@@ -0,0 +1,156 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::PdfExport::ExportCard
+ class CardElement
+ include OpenProject::PdfExport::Exceptions
+
+ def initialize(pdf, orientation, groups_config, work_package)
+ @pdf = pdf
+ @orientation = orientation
+ @groups_config = groups_config
+ @work_package = work_package
+ @group_elements = []
+
+ # Simpler to remove empty rows before calculating the row sizes
+ RowElement.prune_empty_groups(@groups_config, work_package)
+
+ # NEW
+ all_heights = assign_all_heights_new(@groups_config)
+ reduce_rows(all_heights)
+
+ text_padding = @orientation[:text_padding]
+ group_padding = @orientation[:group_padding]
+ current_row = 0
+ current_y_offset = text_padding
+
+ # Initialize groups
+ @groups_config.each_with_index do |(g_key, g_value), i|
+ row_count = g_value["rows"].count
+ row_heights = all_heights[:row_heights].reject {|row| row[:group] != i}.map{|row| row[:height]}
+ group_height = all_heights[:group_heights][i]
+ group_orientation = {
+ y_offset: @orientation[:height] - current_y_offset,
+ x_offset: 0,
+ width: @orientation[:width],
+ height: group_height,
+ row_heights: row_heights,
+ text_padding: text_padding,
+ group_padding: group_padding
+ }
+ @group_elements << GroupElement.new(@pdf, group_orientation, g_value, @work_package)
+
+ current_y_offset += group_height
+ current_row += row_count
+ end
+ end
+
+ def assign_all_heights_new(groups)
+ available = @orientation[:height] - (@orientation[:group_padding] * 2)
+ group_heights = Array.new
+ row_heights = Array.new
+
+ groups.each_with_index do |(gk, gv), i|
+ enforced_group_height = gv["height"] || -1
+ used_group_height = 0
+
+ gv["rows"].each do |rk, rv|
+ if rv["height"]
+ used_group_height += rv["height"]
+ row_heights << { height: rv["height"], group: i, priority: rv["priority"] || 10 }
+ else
+ used_group_height += min_row_height(rv)
+ row_heights << { height: min_row_height(rv), group: i, priority: rv["priority"] || 10 }
+ end
+ end
+
+ group_heights << [used_group_height, enforced_group_height].max
+ end
+
+ { group_heights: group_heights, row_heights: row_heights }
+ end
+
+ def reduce_rows(heights)
+ available = @orientation[:height] - (@orientation[:group_padding] * 2)
+ diff = available - heights[:group_heights].sum
+ return false if diff >= 0
+ diff *= -1
+
+ rows = heights[:row_heights]
+ groups = heights[:group_heights]
+
+ priorities = *(0..rows.count - 1)
+ .zip(rows.map { |row| row[:priority] or 10 })
+ .sort {|x,y| y[1] <=> x[1]}
+ .map {|x| x[0]}
+
+ priorities.each do |p|
+ to_reduce = rows[p]
+ if to_reduce[:height] >= diff
+ to_reduce[:height] -= diff
+ groups[to_reduce[:group]] -= diff
+ break
+ else
+ diff -= to_reduce[:height]
+ groups[to_reduce[:group]] -= to_reduce[:height]
+ to_reduce[:height] = 0
+ end
+ end
+
+ heights
+ end
+
+ def min_row_height(row)
+ return row["enforced_group_height"] if row["enforced_group_height"]
+
+ # Look through each of the row's columns for the column with the largest minimum height
+ largest = 0
+ row["columns"].each do |rk, rv|
+ min_lines = rv["minimum_lines"] || 1
+ font_size = rv["min_font_size"] || rv["font_size"] || 10
+ min_col_height = (@pdf.font.height_at(font_size) * min_lines).floor
+ largest = min_col_height if min_col_height > largest
+ end
+ largest
+ end
+
+ def draw
+ top_left = [@orientation[:x_offset], @orientation[:y_offset]]
+ bounds = @orientation.slice(:width, :height)
+
+ @pdf.bounding_box(top_left, bounds) do
+ @pdf.stroke_color '000000'
+
+ # Draw rows
+ @group_elements.each do |group|
+ group.draw
+ end
+
+ @pdf.stroke_bounds
+ end
+
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/column_element.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/column_element.rb
new file mode 100644
index 0000000000..15ded5ab7b
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/column_element.rb
@@ -0,0 +1,197 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::PdfExport::ExportCard
+ class ColumnElement
+ def initialize(pdf, property_name, config, orientation, work_package)
+ @pdf = pdf
+ @property_name = property_name
+ @config = config
+ @orientation = orientation
+ @work_package = work_package
+ end
+
+ def draw
+ # Get value from model
+ if @work_package.respond_to?(@property_name)
+ value = @work_package.send(@property_name)
+ else
+ # Look in Custom Fields
+ value = ""
+ available_languages.each do |locale|
+ I18n.with_locale(locale) do
+ if (customs = @work_package.custom_field_values.select {|cf| cf.custom_field.name == @property_name} and customs.count > 0)
+ value = customs.first.value
+ @custom_field = customs.first.custom_field
+ end
+ end
+ @localised_custom_field_name = @custom_field.name if !!@custom_field
+ end
+ end
+
+ draw_value(value)
+ end
+
+ private
+
+
+ def available_languages
+ Setting.available_languages
+ end
+
+ def label_text(value)
+ if @has_label
+ custom_label = @config['custom_label']
+ label_text = if custom_label
+ "#{custom_label}"
+ else
+ localised_property_name
+ end
+ if @config['has_count'] && value.is_a?(Array)
+ label_text = "#{label_text} (#{value.count})"
+ end
+
+ label_text += ": "
+ else
+ label_text = ""
+ end
+ label_text
+ end
+
+ def abbreviated_text(text, options)
+ options = options.merge!({ document: @pdf })
+ text_box = Prawn::Text::Box.new(text, options)
+ left_over = text_box.render(:dry_run => true)
+
+ # Be sure to do length arithmetics on chars, not bytes!
+ left_over = left_over.mb_chars
+ text = text.to_s.mb_chars
+
+ text = left_over.size > 0 ? text[0 ... -(left_over.size + 5)] + "[...]" : text
+ text.to_s
+ rescue Prawn::Errors::CannotFit
+ ''
+ end
+
+ def localised_property_name
+ @work_package.class.human_attribute_name(@localised_custom_field_name ||= @property_name)
+ end
+
+ def draw_value(value)
+ # Font size
+ if @config['font_size']
+ # Specific size given
+ overflow = :truncate
+ font_size = Integer(@config['font_size'])
+
+ elsif @config['min_font_size']
+ # Range given
+ overflow = :shrink_to_fit
+ min_font_size = Integer(@config['min_font_size'])
+ font_size = if @config['max_font_size']
+ Integer(@config['max_font_size'])
+ else
+ min_font_size
+ end
+ else
+ # Default
+ font_size = 12
+ overflow = :truncate
+ end
+
+ font_style = (@config['font_style'] or "normal").to_sym
+ text_align = (@config['text_align'] or "left").to_sym
+
+ # Label and text
+ @has_label = @config['has_label']
+ indented = @config['indented']
+
+
+ # Flatten value to a display string
+ display_value = value
+ display_value = display_value.map{|c| c.to_s }.join("\n") if display_value.is_a?(Array)
+ display_value = display_value.to_s if !display_value.is_a?(String)
+
+ if @has_label && indented
+ width_ratio = 0.2 # Note: I don't think it's worth having this in the config
+
+ # Label Textbox
+ offset = [@orientation[:x_offset], @orientation[:height] - (@orientation[:text_padding] / 2)]
+ box = @pdf.text_box(label_text(value),
+ {:height => @orientation[:height],
+ :width => @orientation[:width] * width_ratio,
+ :at => offset,
+ :style => :bold,
+ :overflow => overflow,
+ :size => font_size,
+ :min_font_size => min_font_size,
+ :align => :left})
+
+ # Get abbraviated text
+ options = {:height => @orientation[:height],
+ :width => @orientation[:width] * (1 - width_ratio),
+ :at => offset,
+ :style => font_style,
+ :overflow => overflow,
+ :size => font_size,
+ :min_font_size => min_font_size,
+ :align => text_align}
+ text = abbreviated_text(display_value, options)
+ offset = [@orientation[:x_offset] + (@orientation[:width] * width_ratio), @orientation[:height] - (@orientation[:text_padding] / 2)]
+
+ # Content Textbox
+ box = @pdf.text_box(text, {:height => @orientation[:height],
+ :width => @orientation[:width] * (1 - width_ratio),
+ :at => offset,
+ :style => font_style,
+ :overflow => overflow,
+ :size => font_size,
+ :min_font_size => min_font_size,
+ :align => text_align})
+ else
+ options = {:height => @orientation[:height],
+ :width => @orientation[:width],
+ :at => offset,
+ :style => font_style,
+ :overflow => overflow,
+ :min_font_size => min_font_size,
+ :align => text_align}
+
+ text = abbreviated_text(display_value, options)
+ texts = [{ text: label_text(value), styles: [:bold], :size => font_size }, { text: text, :size => font_size }]
+
+ # Label and Content Textbox
+ offset = [@orientation[:x_offset], @orientation[:height] - (@orientation[:text_padding] / 2)]
+ box = @pdf.formatted_text_box(texts, {:height => @orientation[:height],
+ :width => @orientation[:width],
+ :at => offset,
+ :style => font_style,
+ :overflow => overflow,
+ :min_font_size => min_font_size,
+ :align => text_align})
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/document_generator.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/document_generator.rb
new file mode 100644
index 0000000000..d9b65883f2
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/document_generator.rb
@@ -0,0 +1,105 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'prawn'
+
+module OpenProject::PdfExport::ExportCard
+ require "open_project/pdf_export/export_card/model_display/work_package_display"
+ class DocumentGenerator
+
+ attr_reader :config
+ attr_reader :work_packages
+ attr_reader :pdf
+ attr_reader :current_position
+ attr_reader :paper_width
+ attr_reader :paper_height
+
+ def initialize(config, work_packages)
+ patch_models
+
+ defaults = { page_size: "A4" }
+
+ @config = config
+ @work_packages = work_packages
+
+ page_layout = :landscape if config.landscape? else :portrait
+ page_size = config.page_size or defaults[:page_size]
+ geom = Prawn::Document::PageGeometry::SIZES[page_size]
+ @paper_width = geom[0]
+ @paper_height = geom[1]
+
+ @pdf = Prawn::Document.new(
+ :page_layout => page_layout,
+ :left_margin => 0,
+ :right_margin => 0,
+ :top_margin => 0,
+ :bottom_margin => 0,
+ :page_size => page_size)
+ end
+
+ def render
+ render_pages
+ pdf.render
+ end
+
+ def render_pages
+ card_padding = 10
+ group_padding = 5
+ text_padding = 5
+ card_width = pdf.bounds.width - (card_padding * 2)
+ card_height = ((pdf.bounds.height - (card_padding * config.per_page )) / config.per_page) - (card_padding / config.per_page)
+ card_y_offset = pdf.bounds.height - card_padding
+
+ @work_packages.each_with_index do |wp, i|
+ orientation = {
+ y_offset: card_y_offset,
+ x_offset: card_padding,
+ width: card_width.floor,
+ height: card_height.floor,
+ card_padding: card_padding,
+ group_padding: group_padding,
+ text_padding: text_padding
+ }
+
+ card_element = CardElement.new(pdf, orientation, config.rows_hash, wp)
+ if i > 0 && i % config.per_page == 0
+ pdf.start_new_page
+ end
+ card_element.draw
+
+ if (i + 1) % config.per_page == 0
+ card_y_offset = pdf.bounds.height - card_padding
+ else
+ card_y_offset -= (card_height + card_padding)
+ end
+ end
+ end
+
+ def patch_models
+ # Note: Can't seem to patch the models when initializing for reasons which I don't understand
+ WorkPackage.send(:include, WorkPackageDisplay)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/group_element.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/group_element.rb
new file mode 100644
index 0000000000..6cf6106633
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/group_element.rb
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+module OpenProject::PdfExport::ExportCard
+ class GroupElement
+ include OpenProject::PdfExport::Exceptions
+
+ def initialize(pdf, orientation, config, work_package)
+ @pdf = pdf
+ @orientation = orientation
+ @config = config
+ @rows_config = config["rows"]
+ @work_package = work_package
+ @row_elements = []
+
+ current_y_offset = 0
+ row_heights = @orientation[:row_heights]
+
+ @rows_config.each_with_index do |(r_key, r_value), i|
+ current_y_offset += (row_heights[i - 1]) if i > 0
+ row_orientation = {
+ y_offset: @orientation[:height] - current_y_offset,
+ x_offset: 0,
+ width: @orientation[:width] - (@orientation[:group_padding] * 2),
+ height: row_heights[i],
+ text_padding: @orientation[:text_padding]
+ }
+
+ @row_elements << RowElement.new(@pdf, row_orientation, r_value, @work_package)
+ end
+ end
+
+ def draw
+ padding = @orientation[:group_padding]
+ top_left = [@orientation[:x_offset] + padding, @orientation[:y_offset]]
+ bounds = @orientation.slice(:width, :height)
+ bounds[:width] -= padding * 2
+
+ @pdf.bounding_box(top_left, bounds) do
+ @pdf.stroke_color '000000'
+
+ # Draw rows
+ @row_elements.each do |row|
+ row.draw
+ end
+
+ if (@config["has_border"] or false)
+ @pdf.stroke_bounds
+ end
+ end
+
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/model_display/work_package_display.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/model_display/work_package_display.rb
new file mode 100644
index 0000000000..e24d137fad
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/model_display/work_package_display.rb
@@ -0,0 +1,5 @@
+module WorkPackageDisplay
+ def display_id
+ "#{(kind.is_standard) ? "" : "#{kind.name}"} ##{id}"
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/row_element.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/row_element.rb
new file mode 100644
index 0000000000..c2cfd6ffa1
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/export_card/row_element.rb
@@ -0,0 +1,138 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::PdfExport::ExportCard
+ class RowElement
+ include OpenProject::PdfExport::Exceptions
+
+ def initialize(pdf, orientation, config, work_package)
+ @pdf = pdf
+ @orientation = orientation
+ @config = config
+ @columns_config = config["columns"]
+ @work_package = work_package
+ @column_elements = []
+
+ raise BadlyFormedExportCardConfigurationError.new("Badly formed YAML") if @columns_config.nil?
+
+ # Initialise column elements
+ x_offset = 0
+ padding = @orientation[:text_padding]
+
+ @columns_config.each do |key, value|
+ width = col_width(value) - padding
+ column_orientation = @orientation.clone
+ column_orientation[:x_offset] = x_offset + padding
+ column_orientation[:width] = width - padding
+ x_offset += width + padding
+
+ @column_elements << ColumnElement.new(@pdf, key, value, column_orientation, @work_package)
+ end
+ end
+
+ def col_width(col_config)
+ cols_count = @columns_config.count
+ w = col_config["width"]
+ return @orientation[:width] / cols_count if w.nil?
+
+ i = w.index("%") or w.length
+ Float(w.slice(0, i)) / 100 * @orientation[:width]
+ end
+
+ def draw
+ top_left = [@orientation[:x_offset], @orientation[:y_offset]]
+ bounds = @orientation.slice(:width, :height)
+ @pdf.bounding_box(top_left, bounds) do
+ if @config["has_border"]
+ @pdf.stroke_bounds
+ end
+
+ # Draw columns
+ @column_elements.each do |c|
+ c.draw
+ end
+ end
+
+ end
+
+ def self.prune_empty_groups(groups, wp)
+ # Prune rows in groups
+ groups.each do |gk, gv|
+ self.prune_empty_rows(gv["rows"], wp)
+ end
+
+ # Prune empty groups
+ groups.each do |gk, gv|
+ if gv["rows"].count == 0
+ groups.delete(gk)
+ end
+ end
+ end
+
+ def self.prune_empty_rows(rows, wp)
+ rows.each do |rk, rv|
+ # TODO RS: This is still only checking the first column, need to check all
+ ck, cv = rv["columns"].first
+ if !is_existing_column?(ck, wp) || is_empty_column(ck, cv, wp)
+ rows.delete(rk)
+ end
+ end
+ end
+
+ def self.is_empty_column(property_name, column, wp)
+ if wp.respond_to?(property_name)
+ value = wp.send(property_name)
+ elsif (field = locale_independent_custom_field(property_name, wp)) && !!field
+ value = field.value
+ else
+ value = ""
+ end
+
+ value = "" if value.is_a?(Array) && value.empty?
+ value = value.to_s if !value.is_a?(String)
+
+ !column["render_if_empty"] && value.empty?
+ end
+
+ def self.is_existing_column?(property_name, wp)
+ wp.respond_to?(property_name) || is_existing_custom_field?(property_name, wp)
+ end
+
+ def self.is_existing_custom_field?(property_name, wp)
+ !!locale_independent_custom_field(property_name, wp)
+ end
+
+ def self.locale_independent_custom_field(property_name, wp)
+ Setting.available_languages.each do |locale|
+ I18n.with_locale(locale) do
+ if (fields = wp.custom_field_values.select {|cf| cf.custom_field.name == property_name} and fields.count > 0)
+ return fields.first
+ end
+ end
+ end
+ nil
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/version.rb b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/version.rb
new file mode 100644
index 0000000000..a509b2962c
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/open_project/pdf_export/version.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module PdfExport
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/lib/openproject-pdf_export.rb b/vendored-plugins/openproject-pdf_export/lib/openproject-pdf_export.rb
new file mode 100644
index 0000000000..67245faf4d
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/lib/openproject-pdf_export.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/pdf_export'
diff --git a/vendored-plugins/openproject-pdf_export/openproject-pdf_export.gemspec b/vendored-plugins/openproject-pdf_export/openproject-pdf_export.gemspec
new file mode 100644
index 0000000000..f98826b257
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/openproject-pdf_export.gemspec
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/pdf_export/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-pdf_export"
+ s.version = OpenProject::PdfExport::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/pdf-export"
+ s.summary = 'OpenProject Pdf Export'
+ s.description = "Pdf Export Plugin"
+ s.license = "GPLv3"
+
+ s.files = Dir["{app,config,db,lib,doc}/**/*", "README.md"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_dependency "prawn", "~> 0.14.0"
+
+ s.add_development_dependency "pdf-inspector", "~>1.0.0"
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/controllers/export_card_configurations_controller_spec.rb b/vendored-plugins/openproject-pdf_export/spec/controllers/export_card_configurations_controller_spec.rb
new file mode 100644
index 0000000000..0c4cfbb40a
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/controllers/export_card_configurations_controller_spec.rb
@@ -0,0 +1,171 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+require 'spec_helper'
+require File.dirname(__FILE__) + '/../shared_examples'
+
+describe ExportCardConfigurationsController, :type => :controller do
+ before do
+ allow(@controller).to receive(:require_admin) { true }
+
+ @default_config = FactoryGirl.create(:default_export_card_configuration)
+ @custom_config = FactoryGirl.create(:export_card_configuration)
+ @active_config = FactoryGirl.create(:active_export_card_configuration)
+ @inactive_config = FactoryGirl.create(:inactive_export_card_configuration)
+ @params = {}
+ @valid_rows_yaml = "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ @invalid_rows_yaml = "group1:\n invalid_property: true"
+ @invalid_property_value_format = "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n font_size: sd\n"
+ end
+
+ describe 'Create' do
+ context 'with all the values set' do
+ it_behaves_like "should let you create a configuration" do
+ let(:params) { { :export_card_configuration => { name: "Config 1",
+ description: "This is a description",
+ rows: @valid_rows_yaml,
+ per_page: 5,
+ page_size: "A4",
+ orientation: "landscape" } } }
+ end
+ end
+
+ context 'with missing data' do
+ it_behaves_like "should not let you create a configuration" do
+ let(:params) { { :export_card_configuration => { name: "Config 1" } } }
+ end
+ end
+
+ context 'with invalid data' do
+ it_behaves_like "should not let you create a configuration" do
+ let(:params) { { :export_card_configuration => { name: "Config 1",
+ rows: @invalid_rows_yaml,
+ per_page: 0,
+ page_size: "invalid",
+ orientation: "invalid" } } }
+ end
+ end
+
+ context 'with invalid data format' do
+ it_behaves_like "should not let you create a configuration" do
+ let(:params) { { :export_card_configuration => { name: "Config 1",
+ rows: @invalid_property_value_format,
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape" } } }
+ end
+ end
+ end
+
+ describe 'Update' do
+ it 'should let you update a configuration' do
+ @params[:id] = @custom_config.id
+ @params[:export_card_configuration] = { per_page: 4 }
+ put 'update', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:notice_successful_update))
+ end
+
+ it 'should not let you update a configuration with invalid per_page' do
+ @params[:id] = @custom_config.id
+ @params[:export_card_configuration] = { per_page: 0}
+ put 'update', @params
+
+ expect(response).to render_template('edit')
+ end
+
+ it 'should not let you update a configuration with invalid page_size' do
+ @params[:id] = @custom_config.id
+ @params[:export_card_configuration] = { page_size: "invalid"}
+ put 'update', @params
+
+ expect(response).to render_template('edit')
+ end
+
+ it 'should not let you update a configuration with invalid orientation' do
+ @params[:id] = @custom_config.id
+ @params[:export_card_configuration] = { orientation: "invalid"}
+ put 'update', @params
+
+ expect(response).to render_template('edit')
+ end
+
+ it 'should not let you update a configuration with invalid rows yaml' do
+ @params[:id] = @custom_config.id
+ @params[:export_card_configuration] = { rows: "asdf ',#\""}
+ put 'update', @params
+
+ expect(response).to render_template('edit')
+ end
+ end
+
+ describe 'Delete' do
+ it 'should let you delete a custom configuration' do
+ @params[:id] = @custom_config.id
+ delete 'destroy', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:notice_successful_delete))
+ end
+
+ it 'should not let you delete the default configuration' do
+ @params[:id] = @default_config.id
+ delete 'destroy', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:error_can_not_delete_export_card_configuration))
+ end
+ end
+
+ describe 'Activate' do
+ it 'should let you activate an inactive configuration' do
+ @params[:id] = @inactive_config.id
+ post 'activate', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:notice_export_card_configuration_activated))
+ end
+ end
+
+ describe "Deactivate" do
+ it 'should let you de-activate an active configuration' do
+ @params[:id] = @active_config.id
+ post 'deactivate', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:notice_export_card_configuration_deactivated))
+ end
+
+ it 'should not let you de-activate the default configuration' do
+ @params[:id] = @default_config.id
+ post 'deactivate', @params
+
+ expect(response).to redirect_to :action => 'index'
+ expect(flash[:notice]).to eql(I18n.t(:error_can_not_deactivate_export_card_configuration))
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/export_card/document_generator_spec.rb b/vendored-plugins/openproject-pdf_export/spec/export_card/document_generator_spec.rb
new file mode 100644
index 0000000000..5537563377
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/export_card/document_generator_spec.rb
@@ -0,0 +1,88 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe OpenProject::PdfExport::ExportCard::DocumentGenerator do
+ let(:config) { ExportCardConfiguration.new({
+ name: "Default",
+ description: "This is a description",
+ per_page: 1,
+ page_size: "A4",
+ orientation: "landscape",
+ rows: "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n subject:\n has_label: false\n font_size: 15\n row2:\n height: 50\n priority: 1\n columns:\n non_existent:\n has_label: true\n font_size: 15\n render_if_empty: true"
+ })}
+
+ let(:work_package1) { WorkPackage.new({
+ subject: "Work package 1",
+ description: "This is a description"
+ })}
+
+ let(:work_package2) { WorkPackage.new({
+ subject: "Work package 2",
+ description: "This is work package 2"
+ })}
+
+ describe "Single work package rendering" do
+ before(:each) do
+ work_packages = [work_package1]
+ @generator = OpenProject::PdfExport::ExportCard::DocumentGenerator.new(config, work_packages)
+ end
+
+ it 'shows work package subject' do
+ text_analysis = PDF::Inspector::Text.analyze(@generator.render)
+ expect(text_analysis.strings.include?('Work package 1')).to be_truthy
+ end
+
+ it 'does not show non existent field label' do
+ text_analysis = PDF::Inspector::Text.analyze(@generator.render)
+ expect(text_analysis.strings.include?('Non existent:')).to be_falsey
+ end
+
+ it 'should be 1 page' do
+ page_analysis = PDF::Inspector::Page.analyze(@generator.render)
+ expect(page_analysis.pages.size).to eq(1)
+ end
+ end
+
+ describe "Multiple work package rendering" do
+ before(:each) do
+ work_packages = [work_package1, work_package2]
+ @generator = OpenProject::PdfExport::ExportCard::DocumentGenerator.new(config, work_packages)
+ end
+
+ it 'shows work package subject' do
+ text = PDF::Inspector::Text.analyze(@generator.render)
+ expect(text.strings.include?('Work package 1')).to be_truthy
+ expect(text.strings.include?('Work package 2')).to be_truthy
+ end
+
+ it 'should be 2 pages' do
+ page_analysis = PDF::Inspector::Page.analyze(@generator.render)
+ expect(page_analysis.pages.size).to eq(2)
+ end
+ end
+
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/factories/export_card_configuration_factory.rb b/vendored-plugins/openproject-pdf_export/spec/factories/export_card_configuration_factory.rb
new file mode 100644
index 0000000000..444e04e81a
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/factories/export_card_configuration_factory.rb
@@ -0,0 +1,75 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+FactoryGirl.define do
+ factory :export_card_configuration do
+ name "Config 1"
+ description "This is a description"
+ rows "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ per_page 5
+ page_size "A4"
+ orientation "landscape"
+ end
+
+ factory :default_export_card_configuration, :class => ExportCardConfiguration do
+ name "Default"
+ description "This is a description"
+ active true
+ rows "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ per_page 5
+ page_size "A4"
+ orientation "landscape"
+ end
+
+ factory :invalid_export_card_configuration, :class => ExportCardConfiguration do
+ name "Invalid"
+ description "This is a description"
+ rows "row1"
+ per_page "string"
+ page_size "asdf"
+ orientation "qwer"
+ end
+
+ factory :active_export_card_configuration, :class => ExportCardConfiguration do
+ name "Config active"
+ description "This is a description"
+ active true
+ rows "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ per_page 5
+ page_size "A4"
+ orientation "landscape"
+ end
+
+ factory :inactive_export_card_configuration, :class => ExportCardConfiguration do
+ name "Config inactive"
+ description "This is a description"
+ active false
+ rows "group1:\n has_border: false\n rows:\n row1:\n height: 50\n priority: 1\n columns:\n id:\n has_label: false"
+ per_page 5
+ page_size "A4"
+ orientation "landscape"
+ end
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/shared_examples.rb b/vendored-plugins/openproject-pdf_export/spec/shared_examples.rb
new file mode 100644
index 0000000000..e6d31dd162
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/shared_examples.rb
@@ -0,0 +1,19 @@
+shared_examples_for "should let you create a configuration" do
+
+ before do
+ post 'create', params
+ end
+
+ it { expect(response).to redirect_to :action => 'index' }
+ it { expect(flash[:notice]).to eq(I18n.t(:notice_successful_create)) }
+end
+
+shared_examples_for "should not let you create a configuration" do
+
+ before do
+ post 'create', params
+ end
+
+ it { expect(response).to render_template('new') }
+ it {expect(assigns(:config).errors.messages).to_not be_empty}
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/spec_helper.rb b/vendored-plugins/openproject-pdf_export/spec/spec_helper.rb
new file mode 100644
index 0000000000..e48908169f
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/spec_helper.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+# -- load spec_helper from OpenProject core
+require "spec_helper"
+
+# load gem dependencies
+require "prawn"
+require "pdf/inspector"
diff --git a/vendored-plugins/openproject-pdf_export/spec/views/edit.html.erb_spec.rb b/vendored-plugins/openproject-pdf_export/spec/views/edit.html.erb_spec.rb
new file mode 100644
index 0000000000..2dfe403bdc
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/views/edit.html.erb_spec.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+require 'spec_helper'
+
+describe 'export_card_configurations/edit', :type => :view do
+ let(:config) { FactoryGirl.build(:export_card_configuration) }
+
+ before do
+ config.save
+ assign(:config, config)
+ end
+
+ it 'shows edit export card configuration inputs' do
+ render
+
+ expect(rendered).to have_field("Name", with: config.name)
+ expect(rendered).to have_field("Description", with: config.description)
+ expect(rendered).to have_field("Per page", with: config.per_page.to_s)
+ expect(rendered).to have_field("Page size", with: config.page_size)
+ expect(rendered).to have_field("Orientation", with: config.orientation)
+ expect(rendered).to have_field("Rows", with: config.rows)
+ end
+
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/views/index.html.erb_spec.rb b/vendored-plugins/openproject-pdf_export/spec/views/index.html.erb_spec.rb
new file mode 100644
index 0000000000..662007b6a7
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/views/index.html.erb_spec.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+require 'spec_helper'
+
+describe 'export_card_configurations/index', :type => :view do
+ let(:config1) { FactoryGirl.build(:export_card_configuration, name: "Config 1") }
+ let(:config2) { FactoryGirl.build(:export_card_configuration, name: "Config 2") }
+
+ before do
+ config1.save
+ config2.save
+ assign(:configs, [config1, config2])
+ end
+
+ it 'shows export card configurations' do
+ render
+
+ expect(rendered).to have_selector("a", text: config1.name)
+ expect(rendered).to have_selector("a", text: config2.name)
+ end
+
+end
diff --git a/vendored-plugins/openproject-pdf_export/spec/views/new.html.erb_spec.rb b/vendored-plugins/openproject-pdf_export/spec/views/new.html.erb_spec.rb
new file mode 100644
index 0000000000..2d7daabd30
--- /dev/null
+++ b/vendored-plugins/openproject-pdf_export/spec/views/new.html.erb_spec.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject PDF Export Plugin
+#
+# Copyright (C)2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License version 3.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+
+require 'spec_helper'
+
+describe 'export_card_configurations/new', :type => :view do
+ let(:config) { FactoryGirl.build(:export_card_configuration) }
+
+ before do
+ assign(:config, config)
+ end
+
+ it 'shows new export card configuration empty inputs' do
+ render
+
+ expect(rendered).to have_css("input#export_card_configuration_name")
+ expect(rendered).to have_css("input#export_card_configuration_per_page")
+ expect(rendered).to have_css("input#export_card_configuration_page_size")
+ expect(rendered).to have_css("select#export_card_configuration_orientation")
+ expect(rendered).to have_css("textarea#export_card_configuration_rows")
+ end
+
+end
diff --git a/vendored-plugins/openproject-reporting/.hound.yml b/vendored-plugins/openproject-reporting/.hound.yml
new file mode 100644
index 0000000000..c67bfecae9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
\ No newline at end of file
diff --git a/vendored-plugins/openproject-reporting/.rubocop.yml b/vendored-plugins/openproject-reporting/.rubocop.yml
new file mode 100644
index 0000000000..02f66741c9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - '*.gemspec'
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 100
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
\ No newline at end of file
diff --git a/vendored-plugins/openproject-reporting/.travis.yml b/vendored-plugins/openproject-reporting/.travis.yml
new file mode 100644
index 0000000000..d8a20e3fc2
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/.travis.yml
@@ -0,0 +1,111 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+# Travis configuration based on the respective OpenProject core configuration.
+# Everything save for the matrix section and additional `before_install`
+# instructions is copied and pasted from the core.
+
+language: ruby
+
+rvm:
+ - 2.2.3
+
+sudo: false
+
+cache:
+ - bundler: true
+ - directories:
+ - frontend/node_modules
+ - frontend/bower_components
+
+bundler_args: --without development production
+
+branches:
+ only:
+ - master
+ - dev
+ - /^(stable|release)\/.*$/
+
+env:
+ global:
+ - CI=true
+ - RAILS_ENV=test
+ - COVERAGE=true
+
+ matrix:
+ - "TEST_SUITE=plugins:spec DB=mysql"
+ - "TEST_SUITE=plugins:cucumber DB=mysql"
+
+before_install:
+ # Custom plugin instructions follow.
+
+ # Move the plugin into a subfolder. The plugin-provided Gemfile.plugins
+ # must refer to this folder.
+ - mkdir -p plugins/this
+ - echo `ls -a | tail -n+3 | grep -v plugins` plugins/this/ | xargs mv
+
+ # Get OpenProject.
+ # Doing the fetch detour as you cannot clone into the current directory.
+ - git init
+ - git remote add openproject https://github.com/opf/openproject.git
+ - git fetch --depth=1 openproject
+ - git checkout openproject/dev
+
+ # End of custom plugin instructions.
+
+ - "echo `firefox -v`"
+ - "export DISPLAY=:99.0"
+ - "/sbin/start-stop-daemon --start -v --pidfile ./tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x16"
+ - "echo `xdpyinfo -display :99 | grep 'dimensions' | awk '{ print $2 }'`"
+ - travis_retry npm install
+
+ # We need phantomjs 2.0 to get tests passing
+ - mkdir travis-phantomjs
+ - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2
+ - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs
+ - export PATH=$PWD/travis-phantomjs:$PATH
+
+before_script:
+ - sh script/ci_setup.sh $DB
+
+script:
+ - sh script/ci_runner.sh $TEST_SUITE $GROUP_SIZE $GROUP
+
+notifications:
+ email: false
+ slack:
+ secure: "a+I0uMgXgrDd3aitr2yhXrh7g/UOUTwoDVElunY7gYdrM+gpZ6RE1AP4/Q++hERBCs7rUBzmb//zxGTcc8Nw4nGqZOmPOMIsAoD49UupGLUzHbxzKlpwdBcwh77fq3rYwkjZjE/H1qiElPT7v6qyWMSdNGlj/bAB74eD7Zl3S5cMRvZ1whbSg2GA2v6ZqtXaKfrSFrPRzsIOBXs99OxWNWAsUGpEwTYac7wb6rdMJkBbzosp4gP99mGvQArEzo0nrIQgRH8W4Q6iLnrpX0g5uKccWl1u/G2bmH8L4F50ce4uuUE+TtHO/nfNFnb2KuDR4QyoccQQbGHXL/jaaAZXG/gzs5Hmru2Thaym43fSwxos80xmZs1vqB/rXE+Rg9qXcCKyyX31zjSI/iW4wS015fz8MKVX6qDg49epaw1ovn0AOYrvTd94GV6RX6eJ3/l+KJJHSKaaLP/713h11LWx/S27tiB40fboXQ68YzIQCuahRUEHUfhU3P10Wf9y2fdDsthtHHSrOJMQ3Ii/Jm3KQm6bE5RWORdHvc/sF2WLfLmJ627j9JhWYYi5mDKJ9AeMWtZNHreU0mM27OUgfhiW11ItKgpwQPEiiicrlYRrMmK+9hc9cym+8tRM+wEth1xhIkfgQFtngONKjv361Wt3JifxM79+bn0IyF72vAVNy8k="
+
+addons:
+ firefox: "38.0esr"
+ postgresql: "9.3"
+
+# Disabling coverage reporting until CodeClimate supports merging results from multiple partial tests
+# code_climate:
+# repo_token:
+# secure: "W/lyd8Ud18GRASuVShsIKa2MRHhxjh8WICMQ4WKr68qt0X0Tlp7Bclv4ReiEgiQeKsIoJJy5FfJfINdAT8A4sy2JbrLeISShcIU7Kqpfh6DSLNoRAuLz5P7EXMNFns1gBKCmrSzcB+9ksuTLyTCKkjUcj1NbJzGqpB4jSTecAdg="
diff --git a/vendored-plugins/openproject-reporting/Gemfile.plugins b/vendored-plugins/openproject-reporting/Gemfile.plugins
new file mode 100644
index 0000000000..094337c260
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/Gemfile.plugins
@@ -0,0 +1,11 @@
+# Dependencies (need to be called before the actual gem)
+group :opf_plugins do
+ gem "openproject-costs", :git => "https://github.com/finnlabs/openproject-costs.git", :branch => "dev"
+ gem "reporting_engine", :git => "https://github.com/finnlabs/reporting_engine.git", :branch => "dev"
+
+ # Used by travis to bundle this plugin with the OpenProject core.
+ # The tested plugin will be moved to the path `./plugins/this`
+ # whereas OpenProject will be checked out to `.`.
+
+ gem 'openproject-reporting', path: 'plugins/this'
+end
diff --git a/vendored-plugins/openproject-reporting/README.md b/vendored-plugins/openproject-reporting/README.md
new file mode 100644
index 0000000000..cb060b0ddd
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/README.md
@@ -0,0 +1,84 @@
+OpenProject Reporting Plugin
+=============================
+
+The OpenProject Reporting plugin allows to create custom reports for costs associated to projects using the [OpenProject Costs plugin](https://community.openproject.org/projects/costs-plugin). Various attributes including custom fields can be used to filter the data and the results can be grouped by these attributes.
+
+The OpenProject Reporting plugin is built on top of the [ReportingEngine Rails engine](https://community.openproject.org/projects/plugin-reportingengine), providing the base functionality for customized database reports.
+
+Requirements
+------------
+
+The OpenProject Reporting plugin requires the [OpenProject Core](https://github.com/opf/openproject/) in
+version greater or equal to *3.0.0*. It also requires the [ReportingEngine Rails engine](https://github.com/finnlabs/reporting_engine.git) in version greater or equal to *1.0.0*. Finally, it also requires the [OpenProject Costs plugin](https://github.com/finnlabs/openproject-costs.git).
+
+Installation
+------------
+
+Reporting depends on the OpenProject Costs plugin. If you have not installed it yet, you can do so by adding the following line to the `Gemfile.plugins` in your OpenProject installation:
+
+`gem "openproject-costs", git: "https://github.com/finnlabs/openproject-costs.git", :branch => "stable"`
+
+Furthermore, OpenProject reporting depends on the ReportingEngine which should be installed by adding the following line to your `Gemfile.plugins` in your OpenProject installation folder:
+
+`gem "reporting_engine", git: "https://github.com/finnlabs/reporting_engine.git", :branch => "stable"`
+
+Finally, add the following line to your `Gemfile.plugins` in your OpenProject installation folder to use the Reporting plugin:
+
+`gem "openproject-reporting", git: "https://github.com/finnlabs/openproject-reporting.git", :branch => "stable"`
+
+Afterwards, run:
+
+`bundle install`
+
+
+Deinstallation
+--------------
+
+Remove the lines
+
+`gem "reporting_engine", git: "https://github.com/finnlabs/reporting_engine.git", :branch => "stable"`
+`gem "openproject-reporting", git: "https://github.com/finnlabs/openproject-reporting.git", :branch => "stable"`
+
+from your `Gemfile.plugins` in your OpenProject installation folder and run:
+
+`bundle install`
+
+to uninstall the ReportingEngine and the OpenProject Reporting plugin.
+
+
+Configuration
+-------------
+
+* `cost_reporting_cache_filter_classes: true`
+
+OpenProject Reporting, when not configured otherwise, optimizes response time by caching the filters and group by options generated for work package custom fields. Only when the custom fields are invalidated, does reporting recreate the elements by information from the database. In some scenarios, such a behavior might not be desirable. Especially, when databases are switched between requests to serve information from another installation, caching will almost always fail as the information is outdated and in some edge cases, filters and group by options are displayed erroneously. In such a setting, it is advisible to deactivate the caching by setting `cost_reporting_cache_filter_classes` to `false` in OpenProject's `config/configuration.yml`
+
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/plugin-reporting
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/openproject-reporting`
+
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+
+Licence
+-------
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/openproject-reporting/app/controllers/cost_reports_controller.rb b/vendored-plugins/openproject-reporting/app/controllers/cost_reports_controller.rb
new file mode 100644
index 0000000000..795d41a3d9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/controllers/cost_reports_controller.rb
@@ -0,0 +1,260 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostReportsController < ApplicationController
+ rescue_from Exception do |exception|
+ session.delete(CostQuery.name.underscore.to_sym)
+ raise exception
+ end
+
+ rescue_from ActiveRecord::RecordNotFound do |_exception|
+ render_404
+ end
+
+ Widget::Base.dont_cache!
+
+ before_filter :check_cache
+ before_filter :load_all
+ before_filter :find_optional_project
+ before_filter :find_optional_user
+ include Report::Controller
+ before_filter :set_cost_types # has to be set AFTER the Report::Controller filters run
+
+ verify method: :delete, only: %w[destroy]
+ verify method: :post, only: %w[create, update, rename]
+
+ helper_method :cost_types
+ helper_method :cost_type
+ helper_method :unit_id
+ helper_method :public_queries
+ helper_method :private_queries
+
+ attr_accessor :cost_types, :unit_id, :cost_type
+
+ # Checks if custom fields have been updated, added or removed since we
+ # last saw them, to rebuild the filters and group bys.
+ # Called once per request.
+ def check_cache
+ CostQuery::Cache.check
+ end
+
+ ##
+ # @Override
+ # Use respond_to hook, so redmine_export can hook up the excel exporting
+ def index
+ super
+ respond_to do |format|
+ format.html {
+ session[report_engine.name.underscore.to_sym].try(:delete, :name)
+ render action: 'index'
+ }
+ end unless performed?
+ end
+
+ def drill_down
+ redirect_to action: :index
+ end
+
+ ##
+ # Determines if the request sets a unit type
+ def set_unit?
+ params[:unit]
+ end
+
+ ##
+ # @Override
+ # We cannot show a progressbar in Redmine, due to Prototype being less than 1.7
+ def no_progress?
+ true
+ end
+
+ ##
+ # Set a default query to cut down initial load time
+ def default_filter_parameters
+ { operators: { user_id: '=', spent_on: '>d' },
+ values: { user_id: [User.current.id], spent_on: [30.days.ago.strftime('%Y-%m-%d')] }
+ }.tap do |hash|
+ if @project
+ hash[:operators].merge! project_id: '='
+ hash[:values].merge! project_id: [@project.id]
+ end
+ end
+ end
+
+ ##
+ # Set a default query to cut down initial load time
+ def default_group_parameters
+ { columns: [:week], rows: [] }.tap do |h|
+ if @project
+ h[:rows] << :work_package_id
+ else
+ h[:rows] << :project_id
+ end
+ end
+ end
+
+ ##
+ # We apply a project filter, except when we are just applying a brand new query
+ def ensure_project_scope!(filters)
+ return unless ensure_project_scope?
+ if @project
+ filters[:operators].merge! project_id: '='
+ filters[:values].merge! project_id: @project.id.to_s
+ else
+ filters[:operators].delete :project_id
+ filters[:values].delete :project_id
+ end
+ end
+
+ def ensure_project_scope?
+ !(set_filter? or set_unit?)
+ end
+
+ ##
+ # Determine active cost types, the currently selected unit and corresponding cost type
+ def set_cost_types
+ set_active_cost_types
+ set_unit
+ set_cost_type
+ end
+
+ # Determine the currently active unit from the parameters or session
+ # sets the @unit_id -> this is used in the index for determining the active unit tab
+ def set_unit
+ @unit_id = if set_unit?
+ params[:unit].to_i
+ elsif @query.present?
+ cost_type_filter = @query.filters.detect { |f| f.is_a?(CostQuery::Filter::CostTypeId) }
+
+ cost_type_filter.values.first.to_i if cost_type_filter
+ end
+
+ @unit_id = -1 unless @cost_types.include? @unit_id
+ end
+
+ # Determine the active cost type, if it is not labor or money, and add a hidden filter to the query
+ # sets the @cost_type -> this is used to select the proper units for display
+ def set_cost_type
+ if @unit_id != 0 && @query
+ @query.filter :cost_type_id, operator: '=', value: @unit_id.to_s, display: false
+ @cost_type = CostType.find(@unit_id) if @unit_id > 0
+ end
+ end
+
+ # set the @cost_types -> this is used to determine which tabs to display
+ def set_active_cost_types
+ unless session[:report] && (@cost_types = session[:report][:filters][:values][:cost_type_id].try(:collect, &:to_i))
+ relevant_cost_types = CostType.select(:id).order('id ASC').select do |t|
+ t.cost_entries.count > 0
+ end.collect(&:id)
+ @cost_types = [-1, 0, *relevant_cost_types]
+ end
+ end
+
+ def load_all
+ CostQuery::GroupBy.all
+ CostQuery::Filter.all
+ end
+
+ # @Override
+ def determine_engine
+ @report_engine = CostQuery
+ @title = "label_#{@report_engine.name.underscore}"
+ end
+
+ # N.B.: Users with save_cost_reports permission implicitly have
+ # save_private_cost_reports permission as well
+ #
+ # @Override
+ def allowed_to?(action, report, user = User.current)
+ # admins may do everything
+ return true if user.admin?
+
+ # If this report does belong to a project but not to the current project, we
+ # should not do anything with it. It fact, this should never happen.
+ return false if report.project.present? && report.project != @project
+
+ # If report does not belong to a project, it is ok to look for the
+ # permission in any project. Otherwise, the user should have the permission
+ # in this project.
+ if report.project.present?
+ options = {}
+ else
+ options = { global: true }
+ end
+
+ case action
+ when :create
+ user.allowed_to?(:save_cost_reports, @project, options) or
+ user.allowed_to?(:save_private_cost_reports, @project, options)
+
+ when :save, :delete, :rename
+ if report.is_public?
+ user.allowed_to?(:save_cost_reports, @project, options)
+ else
+ user.allowed_to?(:save_cost_reports, @project, options) or
+ user.allowed_to?(:save_private_cost_reports, @project, options)
+ end
+
+ when :save_as_public
+ user.allowed_to?(:save_cost_reports, @project, options)
+
+ else
+ false
+ end
+ end
+
+ def public_queries
+ if @project
+ CostQuery.where(['is_public = ? AND (project_id IS NULL OR project_id = ?)', true, @project])
+ .order('name ASC')
+ else
+ CostQuery.where(['is_public = ? AND project_id IS NULL', true])
+ .order('name ASC')
+ end
+ end
+
+ def private_queries
+ if @project
+ CostQuery.where(['user_id = ? AND is_public = ? AND (project_id IS NULL OR project_id = ?)',
+ current_user,
+ false,
+ @project])
+ .order('name ASC')
+ else
+ CostQuery.where(['user_id = ? AND is_public = ? AND project_id IS NULL', current_user, false])
+ .order('name ASC')
+ end
+ end
+
+ def display_report_list
+ report_type = params[:report_type] || :public
+ render partial: 'report_list', locals: { report_type: report_type }, layout: !request.xhr?
+ end
+
+ private
+
+ def find_optional_user
+ @current_user = User.current || User.anonymous
+ end
+
+ def default_breadcrumb
+ l(:cost_reports_title)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/helpers/reporting_helper.rb b/vendored-plugins/openproject-reporting/app/helpers/reporting_helper.rb
new file mode 100644
index 0000000000..00f1288dca
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/helpers/reporting_helper.rb
@@ -0,0 +1,217 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'digest/md5'
+require 'date'
+
+module ReportingHelper
+ # ======================= SHARED CODE START
+ include ApplicationHelper
+ include WorkPackagesHelper
+
+ def with_project(project)
+ project = Project.find(project) unless project.is_a? Project
+ project_was, @project = @project, project
+ yield
+ @project = project_was
+ end
+
+ def mapped(value, klass, default)
+ id = value.to_i
+ return default if id < 0
+ klass.find(id).name
+ end
+
+ def label_for(field)
+ name = field.to_s
+ if name.starts_with?('label')
+ return I18n.t(field)
+ end
+ name = name.camelcase
+ if CostQuery::Filter.const_defined? name
+ CostQuery::Filter.const_get(name).label
+ elsif
+ CostQuery::GroupBy.const_defined? name
+ CostQuery::GroupBy.const_get(name).label
+ else
+ # note that using WorkPackage.human_attribute_name relies on the attribute
+ # being an work_package attribute or a general attribute for all models whicht might not
+ # be the case but so far I have only seen the "comments" attribute in reports
+ WorkPackage.human_attribute_name(field)
+ end
+ end
+
+ def debug_fields(result, prefix = ', ')
+ prefix << result.fields.inspect << ', ' << result.important_fields.inspect << ', ' << result.key.inspect if params[:debug]
+ end
+
+ def month_name(index)
+ Date::MONTHNAMES[index].to_s
+ end
+
+ # ======================= SHARED CODE END
+
+ def show_field(key, value)
+ @show_row ||= Hash.new { |h, k| h[k] = {} }
+ @show_row[key][value] ||= field_representation_map(key, value)
+ end
+
+ def raw_field(key, value)
+ @raw_row ||= Hash.new { |h, k| h[k] = {} }
+ @raw_row[key][value] ||= field_sort_map(key, value)
+ end
+
+ def cost_object_link(cost_object_id)
+ co = CostObject.find(cost_object_id)
+ if User.current.allowed_to?(:view_cost_objects, co.project)
+ link_to_cost_object(co)
+ else
+ co.subject
+ end
+ end
+
+ def field_representation_map(key, value)
+ return l(:label_none) if value.blank?
+ case key.to_sym
+ when :activity_id then mapped value, Enumeration, "#{l(:caption_material_costs)} "
+ when :project_id then link_to_project Project.find(value.to_i)
+ when :user_id, :assigned_to_id, :author_id then link_to_user(User.find_by_id(value.to_i) || DeletedUser.first)
+ when :tyear, :units then h(value.to_s)
+ when :tweek then "#{l(:label_week)} ##{h value}"
+ when :tmonth then month_name(value.to_i)
+ when :category_id then h(Category.find(value.to_i).name)
+ when :cost_type_id then mapped value, CostType, l(:caption_labor)
+ when :cost_object_id then cost_object_link value
+ when :work_package_id then link_to_work_package(WorkPackage.find(value.to_i))
+ when :spent_on then format_date(value.to_date)
+ when :type_id then h(Type.find(value.to_i).name)
+ when :week then "#{l(:label_week)} #%s" % value.to_i.modulo(100)
+ when :priority_id then h(IssuePriority.find(value.to_i).name)
+ when :fixed_version_id then h(Version.find(value.to_i).name)
+ when :singleton_value then ''
+ when :status_id then h(Status.find(value.to_i).name)
+ else h(value.to_s)
+ end
+ end
+
+ def field_sort_map(key, value)
+ return '' if value.blank?
+ case key.to_sym
+ when :work_package_id, :tweek, :tmonth, :week then value.to_i
+ when :spent_on then value.to_date.mjd
+ else h(field_representation_map(key, value).gsub(/<\/?[^>]*>/, ''))
+ end
+ end
+
+ def show_result(row, unit_id = self.unit_id)
+ case unit_id
+ when -1 then l_hours(row.units)
+ when 0 then row.real_costs ? number_to_currency(row.real_costs) : '-'
+ else
+ current_cost_type = @cost_type || CostType.find(unit_id)
+ pluralize(row.units, current_cost_type.unit, current_cost_type.unit_plural)
+ end
+ end
+
+ def set_filter_options(struct, key, value)
+ struct[:operators][key] = '='
+ struct[:values][key] = value.to_s
+ end
+
+ def available_cost_type_tabs(cost_types)
+ tabs = cost_types.to_a
+ tabs.delete 0 # remove money from list
+ tabs.unshift 0 # add money as first tab
+ tabs.map { |cost_type_id| [cost_type_id, cost_type_label(cost_type_id)] }
+ end
+
+ def cost_type_label(cost_type_id, cost_type_inst = nil, _plural = true)
+ case cost_type_id
+ when -1 then l(:caption_labor)
+ when 0 then l(:label_money)
+ else (cost_type_inst || CostType.find(cost_type_id)).name
+ end
+ end
+
+ def link_to_details(result)
+ return '' # unless result.respond_to? :fields # uncomment to display
+ session_filter = { operators: session[:report][:filters][:operators].dup, values: session[:report][:filters][:values].dup }
+ filters = result.fields.inject session_filter do |struct, (key, value)|
+ key = key.to_sym
+ case key
+ when :week
+ set_filter_options struct, :tweek, value.to_i.modulo(100)
+ set_filter_options struct, :tyear, value.to_i / 100
+ when :month, :year
+ set_filter_options struct, :"t#{key}", value
+ when :count, :units, :costs, :display_costs, :sum, :real_costs
+ else
+ set_filter_options struct, key, value
+ end
+ struct
+ end
+ options = { fields: filters[:operators].keys, set_filter: 1, action: :drill_down }
+ link_to '[+]', filters.merge(options), class: 'drill_down', title: l(:description_drill_down)
+ end
+
+ ##
+ # Create the appropriate action for an entry with the type of log to use
+ def action_for(result, options = {})
+ options.merge controller: result.fields['type'] == 'TimeEntry' ? 'timelog' : 'costlog', id: result.fields['id'].to_i
+ end
+
+ ##
+ # Create the appropriate action for an entry with the type of log to use
+ def entry_for(result)
+ type = result.fields['type'] == 'TimeEntry' ? TimeEntry : CostEntry
+ type.find(result.fields['id'].to_i)
+ end
+
+ ##
+ # For a given row, determine how to render it's contents according to usability and
+ # localization rules
+ def show_row(row)
+ row_text = link_to_details(row) << row.render { |k, v| show_field(k, v) }
+ row_text.html_safe
+ end
+
+ def delimit(items, options = {})
+ options[:step] ||= 1
+ options[:delim] ||= '•'
+ delimited = []
+ items.each_with_index do |item, ix|
+ if ix != 0 and ix % options[:step] == 0
+ delimited << " #{options[:delim]} " + item
+ else
+ delimited << item
+ end
+ end
+ delimited
+ end
+
+ ##
+ # Finds the Filter-Class for as specific filter name while being careful with the filter_name parameter as it is user input.
+ def filter_class(filter_name)
+ klass = CostQuery::Filter.const_get(filter_name.to_s.camelize)
+ return klass if klass.is_a? Class
+ nil
+ rescue NameError
+ return nil
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query.rb b/vendored-plugins/openproject-reporting/app/models/cost_query.rb
new file mode 100644
index 0000000000..c872994005
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query.rb
@@ -0,0 +1,49 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery < Report
+ def_delegators :result, :real_costs
+
+ User.before_destroy do |user|
+ CostQuery.delete_all ['user_id = ? AND is_public = ?', user.id, false]
+ CostQuery.where(['user_id = ?', user.id]).update_all ['user_id = ?', DeletedUser.first.id]
+
+ max_query_id = 0
+ while((current_queries = CostQuery.limit(1000)
+ .where(["id > ?", max_query_id])
+ .order("id ASC")).size > 0) do
+
+ current_queries.each do |query|
+ serialized = query.serialized
+
+ serialized[:filters] = serialized[:filters].map do |name, options|
+ options[:values].delete(user.id.to_s) if ["UserId", "AuthorId", "AssignedToId"].include?(name)
+
+ options[:values].nil? || options[:values].size > 0 ?
+ [name, options] :
+ nil
+ end.compact
+
+ CostQuery.where(["id = ?", query.id]).update_all ["serialized = ?", YAML::dump(serialized)]
+
+ max_query_id = query.id
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/cache.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/cache.rb
new file mode 100644
index 0000000000..d7136038ca
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/cache.rb
@@ -0,0 +1,75 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostQuery::Cache
+ class << self
+
+ def check
+ reset! if reset_required?
+ end
+
+ def reset!
+ update_reset_on
+
+ CostQuery::Filter.reset!
+ CostQuery::Filter::CustomFieldEntries.reset!
+ CostQuery::GroupBy.reset!
+ CostQuery::GroupBy::CustomFieldEntries.reset!
+ end
+
+ protected
+
+ attr_accessor :latest_custom_field_change,
+ :custom_field_count
+
+ def invalid?
+ changed_on = fetch_latest_custom_field_change
+ field_count = fetch_current_custom_field_count
+
+ latest_custom_field_change != changed_on ||
+ custom_field_count != field_count
+ end
+
+ def update_reset_on
+ return if caching_disabled?
+
+ self.latest_custom_field_change = fetch_latest_custom_field_change
+ self.custom_field_count = fetch_current_custom_field_count
+ end
+
+ def fetch_latest_custom_field_change
+ WorkPackageCustomField.maximum(:updated_at)
+ end
+
+ def fetch_current_custom_field_count
+ WorkPackageCustomField.count
+ end
+
+ def caching_disabled?
+ !OpenProject::Configuration.cost_reporting_cache_filter_classes
+ end
+
+ def reset_required?
+ caching_disabled? || invalid?
+ end
+ end
+
+ # initialize to 0 to avoid forced cache reset on first request
+ self.custom_field_count = 0
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/custom_field_mixin.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/custom_field_mixin.rb
new file mode 100644
index 0000000000..3fa7eb5e38
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/custom_field_mixin.rb
@@ -0,0 +1,114 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostQuery::CustomFieldMixin
+ include Report::QueryUtils
+
+ attr_reader :custom_field
+ SQL_TYPES = {
+ 'string' => mysql? ? 'char' : 'varchar',
+ 'list' => mysql? ? 'char' : 'varchar',
+ 'text' => mysql? ? 'char' : 'text',
+ 'bool' => mysql? ? 'unsigned' : 'boolean',
+ 'date' => 'date',
+ 'int' => 'decimal(60,3)',
+ 'float' => 'decimal(60,3)' }
+
+ def self.extended(base)
+ base.inherited_attribute :factory
+ base.factory = base
+ super
+ end
+
+ def all
+ @all ||= generate_subclasses
+ end
+
+ def reset!
+ @all = nil
+
+ remove_subclasses
+ end
+
+ def generate_subclasses
+ WorkPackageCustomField.where(field_format: SQL_TYPES.keys).map do |field|
+ class_name = "CustomField#{field.id}"
+ parent.send(:remove_const, class_name) if parent.const_defined? class_name
+ parent.const_set class_name, Class.new(self)
+ parent.const_get(class_name).prepare(field, class_name)
+ end
+ end
+
+ def remove_subclasses
+ parent.constants.each do |constant|
+ if constant.to_s.match /^CustomField\d+/
+ parent.send(:remove_const, constant)
+ end
+ end
+ end
+
+ def factory?
+ factory == self
+ end
+
+ def on_prepare(&block)
+ return factory.on_prepare unless factory?
+ @on_prepare = block if block
+ @on_prepare ||= proc {}
+ @on_prepare
+ end
+
+ def table_name
+ @class_name.demodulize.underscore.tableize.singularize
+ end
+
+ def label
+ @custom_field.name
+ end
+
+ def prepare(field, class_name)
+ @custom_field = field
+ @class_name = class_name
+ dont_inherit :group_fields
+ db_field table_name
+ join_table (<<-SQL % [CustomValue.table_name, table_name, field.id, field.name, SQL_TYPES[field.field_format]]).gsub(/^ /, '')
+ -- BEGIN Custom Field Join: "%4$s"
+ LEFT OUTER JOIN (
+ \tSELECT
+ \t\tCAST(value AS %5$s) AS %2$s,
+ \t\tcustomized_type,
+ \t\tcustom_field_id,
+ \t\tcustomized_id
+ \tFROM
+ \t\t%1$s)
+ AS %2$s
+ ON %2$s.customized_type = 'WorkPackage'
+ AND %2$s.custom_field_id = %3$d
+ AND %2$s.customized_id = entries.work_package_id
+ -- END Custom Field Join: "%4$s"
+ SQL
+ instance_eval(&on_prepare)
+ self
+ end
+
+ def new(*)
+ fail "Only subclasses of #{self} should be instanciated." if factory?
+ super
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter.rb
new file mode 100644
index 0000000000..6bdd3c1ca9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require "set"
+
+class CostQuery::Filter < Report::Filter
+ def self.all
+ @all ||= super + Set[
+ CostQuery::Filter::ActivityId,
+ CostQuery::Filter::AssignedToId,
+ CostQuery::Filter::AuthorId,
+ CostQuery::Filter::CategoryId,
+ CostQuery::Filter::CostTypeId,
+ CostQuery::Filter::CreatedOn,
+ CostQuery::Filter::DueDate,
+ CostQuery::Filter::FixedVersionId,
+ CostQuery::Filter::WorkPackageId,
+ CostQuery::Filter::OverriddenCosts,
+ CostQuery::Filter::PriorityId,
+ CostQuery::Filter::ProjectId,
+ CostQuery::Filter::SpentOn,
+ CostQuery::Filter::StartDate,
+ CostQuery::Filter::StatusId,
+ CostQuery::Filter::Subject,
+ CostQuery::Filter::TypeId,
+ CostQuery::Filter::UpdatedOn,
+ CostQuery::Filter::UserId,
+ CostQuery::Filter::PermissionFilter,
+ *CostQuery::Filter::CustomFieldEntries.all
+ ]
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/activity_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/activity_id.rb
new file mode 100644
index 0000000000..9d4bc17f66
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/activity_id.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::ActivityId < Report::Filter::Base
+ def self.label
+ TimeEntry.human_attribute_name(:activity)
+ end
+
+ def self.available_values(*)
+ TimeEntryActivity.order('name').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/assigned_to_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/assigned_to_id.rb
new file mode 100644
index 0000000000..65abf7ad7b
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/assigned_to_id.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::AssignedToId < Report::Filter::Base
+ use :null_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:assigned_to)
+ end
+
+ def self.available_values(*)
+ CostQuery::Filter::UserId.available_values
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/author_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/author_id.rb
new file mode 100644
index 0000000000..dca1ca051d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/author_id.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::AuthorId < Report::Filter::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:author)
+ end
+
+ def self.available_values(*)
+ CostQuery::Filter::UserId.available_values
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/category_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/category_id.rb
new file mode 100644
index 0000000000..af27a93436
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/category_id.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::CategoryId < Report::Filter::Base
+ use :null_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:category)
+ end
+
+ def self.available_values(*)
+ categories = Category.where(project_id: Project.visible.map(&:id))
+ categories.map { |c| ["#{c.project.name} - #{c.name} ", c.id] }.sort_by { |a| a.first.to_s + a.second.to_s }
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_object_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_object_id.rb
new file mode 100644
index 0000000000..328ea5b2ce
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_object_id.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::CostObjectId < Report::Filter::Base
+ join_table Project
+ applies_for :label_work_package_attributes
+
+ def self.label
+ CostObject.model_name.human
+ end
+
+ def self.available_values(*)
+ [[l(:caption_labor), -1]] + CostObject.order('name').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_type_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_type_id.rb
new file mode 100644
index 0000000000..b212a4d59d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/cost_type_id.rb
@@ -0,0 +1,44 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::CostTypeId < Report::Filter::Base
+ extra_options :display
+ selectable false
+
+ def self.label
+ WorkPackage.human_attribute_name(:cost_type)
+ end
+
+ def initialize(child = nil, options = {})
+ super
+ @display = options[:display]
+ end
+
+ ##
+ # @Override
+ # Displayability is decided on the instance
+ def display?
+ return super if @display.nil?
+ @display
+ end
+
+ def self.available_values(*)
+ [[::I18n.t(:caption_labor), -1]] + CostType.order('name').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/created_on.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/created_on.rb
new file mode 100644
index 0000000000..2183cb61ad
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/created_on.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::CreatedOn < Report::Filter::Base
+ db_field 'entries.created_on'
+ use :time_operators
+
+ def self.label
+ WorkPackage.human_attribute_name(:created_on)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/custom_field_entries.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/custom_field_entries.rb
new file mode 100644
index 0000000000..451d496255
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/custom_field_entries.rb
@@ -0,0 +1,42 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::CustomFieldEntries < Report::Filter::Base
+ extend CostQuery::CustomFieldMixin
+
+ on_prepare do
+ applies_for :label_work_package_attributes
+ # redmine internals just suck
+ case custom_field.field_format
+ when 'string', 'text' then use :string_operators
+ when 'list' then use :null_operators
+ when 'date' then use :time_operators
+ when 'int', 'float' then use :integer_operators
+ when 'bool'
+ @possible_values = [['true', 't'], ['false', 'f']]
+ use :null_operators
+ else
+ fail "cannot handle #{custom_field.field_format.inspect}"
+ end
+ end
+
+ def self.available_values(*)
+ @possible_values || custom_field.possible_values
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/due_date.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/due_date.rb
new file mode 100644
index 0000000000..f16c8446e9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/due_date.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::DueDate < Report::Filter::Base
+ use :time_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:due_date)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/fixed_version_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/fixed_version_id.rb
new file mode 100644
index 0000000000..88d00941e0
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/fixed_version_id.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::FixedVersionId < Report::Filter::Base
+ use :null_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:fixed_version)
+ end
+
+ def self.available_values(*)
+ versions = Version.where(project_id: Project.visible.map(&:id))
+ versions.map { |a| ["#{a.project.name} - #{a.name}", a.id] }.sort_by { |a| a.first.to_s + a.second.to_s }
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/no_filter.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/no_filter.rb
new file mode 100644
index 0000000000..d55f4c730b
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/no_filter.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::NoFilter < Report::Filter::NoFilter
+ table_name 'entries'
+ dont_display!
+ singleton
+
+ def sql_statement
+ CostQuery::SqlStatement.for_entries
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/overridden_costs.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/overridden_costs.rb
new file mode 100644
index 0000000000..270aeb98a8
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/overridden_costs.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::OverriddenCosts < Report::Filter::Base
+ def self.label
+ CostEntry.human_attribute_name(:overridden_costs)
+ end
+
+ def self.available_operators
+ ['y', 'n'].map(&:to_operator)
+ end
+
+ def self.available_values(*)
+ []
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/permission_filter.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/permission_filter.rb
new file mode 100644
index 0000000000..a47d17338d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/permission_filter.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::PermissionFilter < Report::Filter::Base
+ dont_display!
+ not_selectable!
+ db_field ''
+ singleton
+
+ initialize_query_with { |query| query.filter to_s.demodulize.to_sym }
+
+ def permission_statement(permission)
+ User.current.allowed_to_condition_with_project_id(permission).gsub(/(user|project)s?\.id/, '\1_id')
+ end
+
+ def permission_for(type)
+ "((#{permission_statement :"view_own_#{type}_entries"} AND user_id = #{User.current.id}) " \
+ "OR #{permission_statement :"view_#{type}_entries"})"
+ end
+
+ def display_costs
+ "(#{permission_statement :view_hourly_rates} " \
+ "AND #{permission_statement :view_cost_rates}) " \
+ 'OR ' \
+ "(#{permission_statement :view_own_hourly_rate} " \
+ "AND type = 'TimeEntry' AND user_id = #{User.current.id}) " \
+ 'OR ' \
+ "(#{permission_statement :view_cost_rates} " \
+ "AND type = 'CostEntry' AND user_id = #{User.current.id})"
+ end
+
+ def sql_statement
+ super.tap do |query|
+ query.from.each_subselect do |sub|
+ sub.where permission_for(sub == query.from.first ? 'time' : 'cost')
+ sub.select.delete_if { |f| f.end_with? 'display_costs' }
+ sub.select display_costs: switch(display_costs => '1', else: 0)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/priority_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/priority_id.rb
new file mode 100644
index 0000000000..7484d6c490
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/priority_id.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::PriorityId < Report::Filter::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:priority)
+ end
+
+ def self.available_values(*)
+ IssuePriority.order('position DESC').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/project_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/project_id.rb
new file mode 100644
index 0000000000..c6f72852b1
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/project_id.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::ProjectId < Report::Filter::Base
+ db_field 'entries.project_id'
+
+ def self.label
+ Project.model_name.human
+ end
+
+ def self.available_operators
+ ['=', '!', '=_child_projects', '!_child_projects'].map(&:to_operator)
+ end
+
+ ##
+ # Calculates the available values for this filter.
+ # Gives a map of [project_name, project_id, nesting_level_of_project].
+ # The map is sorted such that projects appear in alphabetical order within a nesting level
+ # and so that descendant projects appear after their ancestors.
+ def self.available_values(*)
+ map = []
+ ancestors = []
+ Project.visible.sort_by(&:lft).each do |project|
+ while ancestors.any? && !project.is_descendant_of?(ancestors.last)
+ ancestors.pop
+ end
+ map << [project.name, project.id, { level: ancestors.size }]
+ ancestors << project
+ end
+ map
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/spent_on.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/spent_on.rb
new file mode 100644
index 0000000000..10bec3f408
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/spent_on.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::SpentOn < Report::Filter::Base
+ use :time_operators
+
+ def self.label
+ I18n.t(:label_spent_on_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/start_date.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/start_date.rb
new file mode 100644
index 0000000000..e242aa20d9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/start_date.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::StartDate < Report::Filter::Base
+ use :time_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:start_date)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/status_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/status_id.rb
new file mode 100644
index 0000000000..1c5f611b43
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/status_id.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# we have to require this here because the operators would not be defined otherwise
+require_dependency 'cost_query/operator'
+class CostQuery::Filter::StatusId < Report::Filter::Base
+ available_operators 'c', 'o'
+ join_table WorkPackage, Status => [WorkPackage, :status]
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:status)
+ end
+
+ def self.available_values(*)
+ Status.order('name').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/subject.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/subject.rb
new file mode 100644
index 0000000000..5065bad7c5
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/subject.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::Subject < Report::Filter::Base
+ use :string_operators
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:subject)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tmonth.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tmonth.rb
new file mode 100644
index 0000000000..c10a2e6b7a
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tmonth.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::Tmonth < Report::Filter::Base
+ use :integer_operators
+
+ def self.label
+ I18n.t(:label_month_reporting)
+ end
+
+ def self.available_values(*)
+ 1.upto(12).map { |i| [::I18n.t('date.month_names')[i], i] }
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tweek.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tweek.rb
new file mode 100644
index 0000000000..ca8273a43d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tweek.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::Tweek < Report::Filter::Base
+ use :integer_operators
+
+ def self.label
+ I18n.t(:label_week_reporting)
+ end
+
+ def self.available_values(*)
+ 1.upto(53).map { |i| [i.to_s, i] }
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tyear.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tyear.rb
new file mode 100644
index 0000000000..1091f791cb
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/tyear.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::Tyear < Report::Filter::Base
+ use :integer_operators
+
+ def self.label
+ I18n.t(:label_year_reporting)
+ end
+
+ def self.available_values(*)
+ 1970.upto(Date.today.year).map { |i| [i.to_s, i] }.reverse
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/type_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/type_id.rb
new file mode 100644
index 0000000000..423ca58b95
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/type_id.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::TypeId < Report::Filter::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:type)
+ end
+
+ def self.available_values(*)
+ Type.order('name').pluck(:name, :id)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/updated_on.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/updated_on.rb
new file mode 100644
index 0000000000..99fdfe191f
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/updated_on.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::UpdatedOn < Report::Filter::Base
+ db_field 'entries.updated_on'
+ use :time_operators
+
+ def self.label
+ WorkPackage.human_attribute_name(:updated_on)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/user_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/user_id.rb
new file mode 100644
index 0000000000..1c58411a28
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/user_id.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::UserId < Report::Filter::Base
+ def self.label
+ WorkPackage.human_attribute_name(:user)
+ end
+
+ def self.available_values(*)
+ # All users which are members in projects the user can see.
+ # Excludes the anonymous user
+ users = User.joins(members: :project)
+ .merge(Project.visible)
+ .where.not(id: User.anonymous)
+ .order_by_name
+ .select(User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s) << :id)
+ .distinct
+
+ values = users.map { |u| [u.name, u.id] }
+ values.unshift ["<< #{::I18n.t(:label_me)} >>", User.current.id.to_s] if User.current.logged?
+ values
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/filter/work_package_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/work_package_id.rb
new file mode 100644
index 0000000000..1c25c6d8eb
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/filter/work_package_id.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Filter::WorkPackageId < Report::Filter::Base
+ def self.label
+ WorkPackage.model_name.human
+ end
+
+ def self.available_values(*)
+ work_packages = Project.visible.map { |p| p.work_packages }.flatten.uniq.sort_by { |i| i.id }
+ work_packages.map { |i| [text_for_work_package(i), i.id] }
+ end
+
+ def self.heavy?
+ true
+ end
+ not_selectable! if heavy?
+
+ ##
+ # Overwrites Report::Filter::Base self.label_for_value method
+ # to achieve a more performant implementation
+ def self.label_for_value(value)
+ return nil unless value.to_i.to_s == value.to_s # we expect an work_package-id
+ work_package = WorkPackage.find(value.to_i)
+ [text_for_work_package(work_package), work_package.id] if work_package and work_package.visible?(User.current)
+ end
+
+ def self.text_for_work_package(i)
+ i = i.first if i.is_a? Array
+ str = "##{i.id} "
+ str << (i.subject.length > 30 ? i.subject.first(26) + '...' : i.subject)
+ end
+
+ def self.text_for_id(i)
+ text_for_work_package WorkPackage.find(i)
+ rescue ActiveRecord::RecordNotFound
+ ''
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by.rb
new file mode 100644
index 0000000000..9f12f37ee8
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by.rb
@@ -0,0 +1,46 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require "set"
+
+class CostQuery::GroupBy < Report::GroupBy
+ def self.all
+ @all ||= super + Set[
+ CostQuery::GroupBy::ActivityId,
+ CostQuery::GroupBy::CostObjectId,
+ CostQuery::GroupBy::CostTypeId,
+ CostQuery::GroupBy::FixedVersionId,
+ CostQuery::GroupBy::WorkPackageId,
+ CostQuery::GroupBy::PriorityId,
+ CostQuery::GroupBy::ProjectId,
+ CostQuery::GroupBy::SpentOn,
+ CostQuery::GroupBy::SingletonValue,
+ CostQuery::GroupBy::Tmonth,
+ CostQuery::GroupBy::TypeId,
+ CostQuery::GroupBy::Tyear,
+ CostQuery::GroupBy::UserId,
+ CostQuery::GroupBy::Week,
+ CostQuery::GroupBy::AuthorId,
+ CostQuery::GroupBy::AssignedToId,
+ CostQuery::GroupBy::CategoryId,
+ CostQuery::GroupBy::StatusId,
+ *CostQuery::GroupBy::CustomFieldEntries.all
+ ]
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/activity_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/activity_id.rb
new file mode 100644
index 0000000000..fab3b1808b
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/activity_id.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::ActivityId < Report::GroupBy::Base
+ def self.label
+ TimeEntry.human_attribute_name(:activity)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/assigned_to_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/assigned_to_id.rb
new file mode 100644
index 0000000000..71d937cca6
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/assigned_to_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::AssignedToId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:assigned_to)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/author_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/author_id.rb
new file mode 100644
index 0000000000..a8082a3230
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/author_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::AuthorId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:author)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/category_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/category_id.rb
new file mode 100644
index 0000000000..3856921d22
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/category_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::CategoryId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:category)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_object_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_object_id.rb
new file mode 100644
index 0000000000..441ceb6352
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_object_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::CostObjectId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ CostObject.model_name.human
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_type_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_type_id.rb
new file mode 100644
index 0000000000..9545b6fd96
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/cost_type_id.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::CostTypeId < Report::GroupBy::Base
+
+ def self.label
+ CostType.model_name.human
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/custom_field_entries.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/custom_field_entries.rb
new file mode 100644
index 0000000000..c942f54994
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/custom_field_entries.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::CustomFieldEntries < Report::GroupBy::Base
+ applies_for :label_work_package_attributes
+ extend CostQuery::CustomFieldMixin
+ on_prepare { group_fields table_name }
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/fixed_version_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/fixed_version_id.rb
new file mode 100644
index 0000000000..b576978088
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/fixed_version_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::FixedVersionId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:fixed_version)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/priority_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/priority_id.rb
new file mode 100644
index 0000000000..09966d0f29
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/priority_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::PriorityId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:priority)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/project_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/project_id.rb
new file mode 100644
index 0000000000..a634bea633
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/project_id.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::ProjectId < Report::GroupBy::Base
+
+ def self.label
+ Project.model_name.human
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/ruby_aggregation.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/ruby_aggregation.rb
new file mode 100644
index 0000000000..63b61dfbfd
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/ruby_aggregation.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy
+ module RubyAggregation
+ def responsible_for_sql?
+ false
+ end
+
+ ##
+ # @return [CostQuery::Result] aggregation
+ def compute_result
+ child.result.grouped_by(all_group_fields(false), type, group_fields)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/singleton_value.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/singleton_value.rb
new file mode 100644
index 0000000000..8fb10628a6
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/singleton_value.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::SingletonValue < Report::GroupBy::SingletonValue
+ dont_display!
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/spent_on.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/spent_on.rb
new file mode 100644
index 0000000000..100c1e99f4
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/spent_on.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::SpentOn < Report::GroupBy::Base
+ def self.label
+ I18n.t(:label_spent_on_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/sql_aggregation.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/sql_aggregation.rb
new file mode 100644
index 0000000000..9bd363109a
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/sql_aggregation.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy
+ module SqlAggregation
+ include Report::GroupBy::SqlAggregation
+
+ def sql_statement
+ super.tap do |sql|
+ sql.sum units: :units, real_costs: :real_costs, display_costs: :display_costs
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/status_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/status_id.rb
new file mode 100644
index 0000000000..11c94217b3
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/status_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::StatusId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:status)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tmonth.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tmonth.rb
new file mode 100644
index 0000000000..3561480d3d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tmonth.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::Tmonth < Report::GroupBy::Base
+
+ def self.label
+ I18n.t(:label_month_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tweek.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tweek.rb
new file mode 100644
index 0000000000..16555b1ee2
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tweek.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::Tweek < Report::GroupBy::Base
+
+ def self.label
+ I18n.t(:label_week_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tyear.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tyear.rb
new file mode 100644
index 0000000000..acd77df9ca
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/tyear.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::Tyear < Report::GroupBy::Base
+
+ def self.label
+ I18n.t(:label_year_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/type_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/type_id.rb
new file mode 100644
index 0000000000..9c7ee783a0
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/type_id.rb
@@ -0,0 +1,27 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::TypeId < Report::GroupBy::Base
+ join_table WorkPackage
+ applies_for :label_work_package_attributes
+
+ def self.label
+ WorkPackage.human_attribute_name(:type)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/user_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/user_id.rb
new file mode 100644
index 0000000000..1fede1a5ea
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/user_id.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::UserId < Report::GroupBy::Base
+
+ def self.label
+ WorkPackage.human_attribute_name(:user)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/week.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/week.rb
new file mode 100644
index 0000000000..11711507aa
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/week.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::Week < Report::GroupBy::Base
+ def self.label
+ I18n.t(:label_week_reporting)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/work_package_id.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/work_package_id.rb
new file mode 100644
index 0000000000..ef28bdd1a7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/group_by/work_package_id.rb
@@ -0,0 +1,25 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::GroupBy::WorkPackageId < Report::GroupBy::Base
+
+ def self.label
+ WorkPackage.model_name.human
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/operator.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/operator.rb
new file mode 100644
index 0000000000..c15e724c4e
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/operator.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Operator < Report::Operator
+ # Operators from Redmine
+ new "c", arity: 0, label: :label_closed do
+ def modify(query, field, *values)
+ raise "wrong field" if field.to_s.split('.').last != "status_id"
+ query.where "(#{Status.table_name}.is_closed = #{quoted_true})"
+ query
+ end
+ end
+
+ new "o", arity: 0, label: :label_open do
+ def modify(query, field, *values)
+ raise "wrong field" if field.to_s.split('.').last != "status_id"
+ query.where "(#{Status.table_name}.is_closed = #{quoted_false})"
+ query
+ end
+ end
+
+ new "=_child_projects", validate: :integers, label: :label_is_project_with_subprojects do
+ def modify(query, field, *values)
+ p_ids = []
+ values.each do |value|
+ p_ids += ([value] << Project.find(value).descendants.map{ |p| p.id })
+ end
+ "=".to_operator.modify query, field, p_ids
+ rescue ActiveRecord::RecordNotFound
+ query
+ end
+ end
+
+ new "!_child_projects", validate: :integers, label: :label_is_not_project_with_subprojects do
+ def modify(query, field, *values)
+ p_ids = []
+ values.each do |value|
+ p_ids += ([value] << Project.find(value).descendants.map{ |p| p.id })
+ end
+ "!".to_operator.modify query, field, p_ids
+ rescue ActiveRecord::RecordNotFound
+ query
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/query_utils.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/query_utils.rb
new file mode 100644
index 0000000000..997e0e5edb
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/query_utils.rb
@@ -0,0 +1,52 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module CostQuery::QueryUtils
+ include Redmine::I18n
+ include Report::QueryUtils
+
+ def map_field(key, value)
+ case key.to_s
+ when "user_id" then value ? user_name(value.to_i) : ''
+ when "tweek", "tyear", "tmonth", /_id$/ then value.to_i
+ when "week" then value.to_i.divmod(100)
+ when /_(on|at)$/ then value ? value.to_dateish : Time.at(0)
+ when /^custom_field/ then value.to_s
+ when "singleton_value" then value.to_i
+ else super
+ end
+ end
+
+ def user_name(id)
+ # we have no identity map... :(
+ cache[:user_name][id] ||= User.find(id).name
+ end
+
+ ##
+ # Graceful, internationalized quoted string.
+ #
+ # @see quote_string
+ # @param [Object] str String to quote/translate
+ # @return [Object] Quoted, translated version
+ def quoted_label(ident)
+ "'#{quote_string l(ident)}'"
+ end
+
+ propagate!
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/result.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/result.rb
new file mode 100644
index 0000000000..54cc97f9d7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/result.rb
@@ -0,0 +1,57 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Result < Report::Result
+ module BaseAdditions
+ def inspect
+ "<##{self.class}: @fields=#{fields.inspect} @type=#{type.inspect} " \
+ "@size=#{size} @count=#{count} @units=#{units} @real_costs=#{real_costs}>"
+ end
+
+ def display_costs?
+ display_costs > 0
+ end
+ end
+
+ class Base < Report::Result::Base
+ include BaseAdditions
+ end
+
+ class DirectResult < Report::Result::DirectResult
+ include BaseAdditions
+ def display_costs
+ self["display_costs"].to_i
+ end
+
+ def real_costs
+ (self["real_costs"] || 0).to_d if display_costs? # FIXME: default value here?
+ end
+ end
+
+ class WrappedResult < Report::Result::WrappedResult
+ include BaseAdditions
+ def display_costs
+ (sum_for :display_costs) >= 1 ? 1 : 0
+ end
+
+ def real_costs
+ sum_for :real_costs if display_costs?
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/sql_statement.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/sql_statement.rb
new file mode 100644
index 0000000000..bfceadcf39
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/sql_statement.rb
@@ -0,0 +1,119 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::SqlStatement < Report::SqlStatement
+ COMMON_FIELDS = %w[
+ user_id project_id work_package_id rate_id
+ comments spent_on created_on updated_on tyear tmonth tweek
+ costs overridden_costs type
+ ]
+
+ # flag to mark a reporting query consisting of a union of cost and time entries
+ attr_accessor :entry_union
+
+ def initialize(table, desc = "")
+ super(table, desc)
+ @entry_union = false
+ end
+
+ # this is a hack to ensure that additional joins added by filters do not result
+ # in additional columns being selected.
+ def to_s
+ select(['entries.*']) if select == ['*'] && group_by.empty? && self.entry_union
+ super
+ end
+
+ ##
+ # Generates SqlStatement that maps time_entries and cost_entries to a common structure.
+ #
+ # Mapping for direct fields:
+ #
+ # Result | Time Entires | Cost entries
+ # --------------------------|--------------------------|--------------------------
+ # id | id | id
+ # user_id | user_id | user_id
+ # project_id | project_id | project_id
+ # work_package_id | work_package_id | work_package_id
+ # rate_id | rate_id | rate_id
+ # comments | comments | comments
+ # spent_on | spent_on | spent_on
+ # created_on | created_on | created_on
+ # updated_on | updated_on | updated_on
+ # tyear | tyear | tyear
+ # tmonth | tmonth | tmonth
+ # tweek | tweek | tweek
+ # costs | costs | costs
+ # overridden_costs | overridden_costs | overridden_costs
+ # units | hours | units
+ # activity_id | activity_id | -1
+ # cost_type_id | -1 | cost_type_id
+ # type | "TimeEntry" | "CostEntry"
+ # count | 1 | 1
+ #
+ # Also: This _should_ handle joining activities and cost_types, as the logic differs for time_entries
+ # and cost_entries.
+ #
+ # @param [#table_name] model The model to map
+ # @return [CostQuery::SqlStatement] Generated statement
+ def self.unified_entry(model)
+ table = table_name_for model
+ new(table).tap do |query|
+ query.select COMMON_FIELDS
+ query.desc = "Subquery for #{table}"
+ query.select({
+ count: 1, id: [model, :id], display_costs: 1,
+ real_costs: switch("#{table}.overridden_costs IS NULL" => [model, :costs], else: [model, :overridden_costs]),
+ week: iso_year_week(:spent_on, model),
+ singleton_value: 1 })
+ #FIXME: build this subquery from a sql_statement
+ query.from "(SELECT *, #{typed :text, model.model_name.to_s} AS type FROM #{table}) AS #{table}"
+ send("unify_#{table}", query)
+ end
+ end
+
+ ##
+ # Applies logic for mapping time entries to general entries structure.
+ #
+ # @param [CostQuery::SqlStatement] query The statement to adjust
+ def self.unify_time_entries(query)
+ query.select :activity_id, units: :hours, cost_type_id: -1
+ query.select cost_type: quoted_label(:caption_labor)
+ end
+
+ ##
+ # Applies logic for mapping cost entries to general entries structure.
+ #
+ # @param [CostQuery::SqlStatement] query The statement to adjust
+ def self.unify_cost_entries(query)
+ query.select :units, :cost_type_id, activity_id: -1
+ query.select cost_type: "cost_types.name"
+ query.join CostType
+ end
+
+ ##
+ # Generates a statement based on all entries (i.e. time entries and cost entries) mapped to the general entries structure,
+ # and therefore usable by filters and such.
+ #
+ # @return [CostQuery::SqlStatement] Generated statement
+ def self.for_entries
+ sql = new unified_entry(TimeEntry).union(unified_entry(CostEntry), "entries")
+ sql.entry_union = true
+ sql
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/models/cost_query/table.rb b/vendored-plugins/openproject-reporting/app/models/cost_query/table.rb
new file mode 100644
index 0000000000..a65dbe3a61
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/cost_query/table.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostQuery::Table < Report::Table; end
diff --git a/vendored-plugins/openproject-reporting/app/models/entry.rb b/vendored-plugins/openproject-reporting/app/models/entry.rb
new file mode 100644
index 0000000000..1eb8746b48
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/models/entry.rb
@@ -0,0 +1,108 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Entry
+ [TimeEntry, CostEntry].each { |e| e.send :include, self }
+
+ class Delegator < ActiveRecord::Base
+ # Rails 3.2.13 delegates most of the methods defined here to an
+ # ActiveRecord::Relation (see active_record/querying.rb).
+ # Thus only implementing the four find_x methods isn't enough
+ # Rails 2.3 internally called these e.g. for all().
+ # A quick fix is implementing all(), but we might need to reconsider how we
+ # do the delegation here if more methods were based on the four find_xs.
+ self.abstract_class = true
+ class << self
+ def ===(obj)
+ TimeEntry === obj or CostEntry === obj
+ end
+
+ def calculate(type, *args)
+ a, b = TimeEntry.calculate(type, *args), CostEntry.calculate(type, *args)
+ case type
+ when :sum, :count then a + b
+ when :avg then (a + b) / 2
+ when :min then [a, b].min
+ when :max then [a, b].max
+ else raise NotImplementedError
+ end
+ end
+
+ undef_method :create, :update, :delete, :destroy, :new, :update_counters,
+ :increment_counter, :decrement_counter
+
+ %w[update_all destroy_all delete_all].each do |meth|
+ define_method(meth) { |*args| send_all(meth, *args) }
+ end
+
+ private
+ def all(*args)
+ ActiveSupport::Deprecation.warn('Passing arguments is deprecated') if args.any?
+ find_many :all # *args
+ end
+
+ def count(*args)
+ ActiveSupport::Deprecation.warn('Passing arguments is deprecated') if args.any?
+ find_many :count # *args
+ end
+
+ def find_initial(options) find_one :find_initial, options end
+ def find_last(options) find_one :find_last, options end
+ def find_every(options) find_many :find_every, options end
+ def find_from_ids(args, options) find_many :find_from_ids, options end
+
+ def find_one(*args)
+ TimeEntry.send(*args) || CostEntry.send(*args)
+ end
+
+ def find_many(*args)
+ TimeEntry.send(*args) + CostEntry.send(*args)
+ end
+
+ def send_all(*args)
+ [TimeEntry.send(*args), CostEntry.send(*args)]
+ end
+ end
+ end
+
+ def units
+ super
+ rescue NoMethodError
+ hours
+ end
+
+ def cost_type
+ super
+ rescue NoMethodError
+ end
+
+ def activity
+ super
+ rescue NoMethodError
+ end
+
+ def activity_id
+ super
+ rescue NoMethodError
+ end
+
+ def self.method_missing(*a, &b)
+ Delegator.send(*a, &b)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_category.html.erb b/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_category.html.erb
new file mode 100644
index 0000000000..53e7d4cb0d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_category.html.erb
@@ -0,0 +1,29 @@
+<%#-- copyright
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%# needs locals:
+ report_type: symbol referring to helper methods of the sort #{report_type}_queries
+%>
+
+<% queries = respond_to?(:"#{report_type}_queries") ? send(:"#{report_type}_queries") : [] %>
+<% if queries.any? %>
+ <%= l(:"label_#{report_type}_report_plural") %>
+ <%= render :partial => "report_list", :locals => { :report_type => report_type, :queries => queries } %>
+<% end -%>
diff --git a/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_list.html.erb b/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_list.html.erb
new file mode 100644
index 0000000000..9da7c1e1e7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/views/cost_reports/_report_list.html.erb
@@ -0,0 +1,30 @@
+<%#-- copyright
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% queries ||= respond_to?(:"#{report_type}_queries") ? send(:"#{report_type}_queries") : [] %>
+
diff --git a/vendored-plugins/openproject-reporting/app/views/cost_reports/_reporting_header.html.erb b/vendored-plugins/openproject-reporting/app/views/cost_reports/_reporting_header.html.erb
new file mode 100644
index 0000000000..bc92ac24ea
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/views/cost_reports/_reporting_header.html.erb
@@ -0,0 +1,24 @@
+<%#-- copyright
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<% content_for :header_tags do %>
+ <%= javascript_include_tag "reporting_engine/reporting_engine" %>
+ <%= stylesheet_link_tag 'reporting_engine/reporting_engine' %>
+<% end %>
diff --git a/vendored-plugins/openproject-reporting/app/views/cost_reports/_restore_query.html.erb b/vendored-plugins/openproject-reporting/app/views/cost_reports/_restore_query.html.erb
new file mode 100644
index 0000000000..9769b0099d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/views/cost_reports/_restore_query.html.erb
@@ -0,0 +1,54 @@
+<%#-- copyright
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+
diff --git a/vendored-plugins/openproject-reporting/app/views/cost_reports/index.html.erb b/vendored-plugins/openproject-reporting/app/views/cost_reports/index.html.erb
new file mode 100644
index 0000000000..c25e8206ad
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/app/views/cost_reports/index.html.erb
@@ -0,0 +1,56 @@
+<%#-- copyright
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++#%>
+
+<%= render :partial => 'reporting_header' %>
+<%= include_calendar_headers_tags %>
+
+<% if @custom_errors.present? %>
+ <% @custom_errors.each do |err| %>
+ <%= err %>
+ <% end %>
+<% end %>
+
+<% html_title (@query.persisted? ? "#{l(:label_cost_report)}: #{@query.name}" : l(:label_new_report)) %>
+
+
+ <%= render_widget Widget::Controls::QueryName, @query, :can_rename => allowed_to?(:rename, @query, current_user) %>
+
+<%= render_widget Widget::Settings, @query, :cost_types => @cost_types, :selected_type_id => @unit_id %>
+
+
+
+ <% if @no_progress || true %>
+ <%= render_widget Widget::Table, @query %>
+ <% else %>
+ <%= render_widget Widget::Table::Progressbar, @query %>
+ <% end %>
+
+
+
+<%= call_hook(:view_cost_report_table_bottom) %>
+
+<% content_for :sidebar do %>
+ <% [:private, :public].each do |type| %>
+ <%= render :partial => "report_category", :locals => { :report_type => type } %>
+ <% end %>
+<% end -%>
diff --git a/vendored-plugins/openproject-reporting/config/locales/da.yml b/vendored-plugins/openproject-reporting/config/locales/da.yml
new file mode 100644
index 0000000000..fa053a6467
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/da.yml
@@ -0,0 +1,49 @@
+da:
+ button_save_as: Gem rapport som...
+ comments: Kommentér
+ cost_reports_title: Omkostningsrapporter
+ label_cost_report: Omkostningsrapport
+ description_drill_down: Vis detaljer
+ description_filter_selection: Markering
+ description_multi_select: Vis flere markeringer
+ description_remove_filter: Fjern filter
+ information_restricted_depending_on_permission: Afhængigt af dine tilladelser kan denne side indeholde oplysninger du ikke kan se.
+ label_click_to_edit: Klik for at redigere.
+ label_closed: lukket
+ label_columns: Kolonner
+ label_cost_entry_attributes: Egenskaber for omkostningsposter
+ label_days_ago: i løbet af de seneste dage
+ label_entry: Omkostningspostering
+ label_filter_text: Filtertekst
+ label_filter_value: Værdi
+ label_filters: Filter
+ label_greater: '>'
+ label_is_not_project_with_subprojects: findes ikke (inkluderer underprojekter)
+ label_is_project_with_subprojects: findes (inkluderer underprojekter)
+ label_work_package_attributes: Egenskaber for arbejdspakker
+ label_less: '<'
+ label_money: Kontantværdi
+ label_month_reporting: Måned (Brugt)
+ label_new_report: Ny omkostningsrapport
+ label_open: åben
+ label_operator: Operator
+ label_private_report_plural: Privat omkostningsrapporter
+ label_progress_bar_explanation: Opbygger rapport...
+ label_public_report_plural: Offentlig omkostningsrapport
+ label_really_delete_question: Er du sikker på du vil slette denne rapport?
+ label_rows: Rækker
+ label_saving: Gemmer...
+ label_spent_on_reporting: Dato (Brugt)
+ label_sum: Sum
+ label_units: Enheder
+ label_week_reporting: Uge (Brugt)
+ label_year_reporting: År (Brugt)
+ load_query_question: 'Rapporten vil få %{size} celler og kan tage nogen tid at optegne. Ønsker du stadig at se den?'
+ permission_save_cost_reports: Gem offentlig omkostningsrapport
+ permission_save_private_cost_reports: Gem privat omkostningsrapport
+ project_module_reporting_module: Omkostningsrapporter
+ text_costs_are_rounded_note: De viste værdier er afrundede. Alle beregninger udføres med de præcise værdier.
+ toggle_multiselect: aktivér/deaktivér mulighed for valg af flere
+ units: Enheder
+ validation_failure_date: er ikke en gyldig dato
+ validation_failure_integer: er ikke et gyldigt heltal
diff --git a/vendored-plugins/openproject-reporting/config/locales/de.yml b/vendored-plugins/openproject-reporting/config/locales/de.yml
new file mode 100644
index 0000000000..6c22860746
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/de.yml
@@ -0,0 +1,49 @@
+de:
+ button_save_as: Speichern unter ...
+ comments: Kommentar
+ cost_reports_title: Kostenauswertungen
+ label_cost_report: Kostenauswertung
+ description_drill_down: Details anzeigen
+ description_filter_selection: Auswahl
+ description_multi_select: Mehrfachauswahl anzeigen
+ description_remove_filter: Filter entfernen
+ information_restricted_depending_on_permission: Abhängig von Ihren Berechtigungen können die Informationen auf dieser Seite eingeschränkt sein.
+ label_click_to_edit: Zum Bearbeiten hier klicken.
+ label_closed: Geschlossen
+ label_columns: Spalten
+ label_cost_entry_attributes: Attribute am Kosteneintrag
+ label_days_ago: in den letzten Tagen
+ label_entry: Kosteneintrag
+ label_filter_text: Filtertext
+ label_filter_value: Wert
+ label_filters: Filter
+ label_greater: '>'
+ label_is_not_project_with_subprojects: ist nicht (mit Unterprojekten)
+ label_is_project_with_subprojects: ist (mit Unterprojekten)
+ label_work_package_attributes: Attribute am Ticket
+ label_less: '<'
+ label_money: Geldwert
+ label_month_reporting: Monat der Buchung
+ label_new_report: Neue Kostenauswertung
+ label_open: Offen
+ label_operator: Operator
+ label_private_report_plural: Persönliche Kostenauswertungen
+ label_progress_bar_explanation: Kostenauswertung wird erstellt ...
+ label_public_report_plural: Gemeinsame Kostenauswertungen
+ label_really_delete_question: Diese Kostenauswertung wirklich löschen?
+ label_rows: Zeilen
+ label_saving: Speichern...
+ label_spent_on_reporting: Datum der Buchung
+ label_sum: Summe
+ label_units: Einheiten
+ label_week_reporting: Woche der Buchung
+ label_year_reporting: Jahr der Buchung
+ load_query_question: 'Die Kostenauswertung wird %{size} Tabellen-Zellen haben, was sehr rechenintensiv sein kann. Wollen Sie dennoch versuchen, die Kostenauswertung durch zu führen?'
+ permission_save_cost_reports: Speichern gemeinsamer Kostenauswertungen
+ permission_save_private_cost_reports: Speichern persönlicher Kostenauswertungen
+ project_module_reporting_module: Kostenauswertungen
+ text_costs_are_rounded_note: Die angezeigten Werte sind gerundet. Alle Berechnungen basieren auf ungerundeten Werten.
+ toggle_multiselect: Mehrfachauswahl aktivieren/deaktivieren
+ units: Einheiten
+ validation_failure_date: ist kein gültiges Datum
+ validation_failure_integer: ist keine ganze Zahl
diff --git a/vendored-plugins/openproject-reporting/config/locales/en.yml b/vendored-plugins/openproject-reporting/config/locales/en.yml
new file mode 100644
index 0000000000..0e61e3d0cf
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/en.yml
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+---
+en:
+ button_save_as: "Save report as..."
+
+ comments: "Comment"
+ cost_reports_title: "Cost reports"
+ label_cost_report: "Cost report"
+
+ description_drill_down: "Show details"
+ description_filter_selection: "Selection"
+ description_multi_select: "Show multiselect"
+ description_remove_filter: "Remove filter"
+
+ information_restricted_depending_on_permission: "Depending on your permissions this page might contain restricted information."
+
+ label_click_to_edit: "Click to edit."
+ label_closed: "closed"
+ label_columns: "Columns"
+ label_cost_entry_attributes: "Cost entry attributes"
+ label_days_ago: "during the last days"
+ label_entry: "Cost entry"
+ label_filter_text: "Filter text"
+ label_filter_value: "Value"
+ label_filters: "Filter"
+ label_greater: ">"
+ label_is_not_project_with_subprojects: "is not (includes subprojects)"
+ label_is_project_with_subprojects: "is (includes subprojects)"
+ label_work_package_attributes: "Work package attributes"
+ label_less: "<"
+ label_money: "Cash value"
+ label_month_reporting: "Month (Spent)"
+ label_new_report: "New cost report"
+ label_open: "open"
+ label_operator: "Operator"
+ label_private_report_plural: "Private cost reports"
+ label_progress_bar_explanation: "Generating report..."
+ label_public_report_plural: "Public cost reports"
+ label_really_delete_question: "Are you sure you want to delete this report?"
+ label_rows: "Rows"
+ label_saving: "Saving ..."
+ label_spent_on_reporting: "Date (Spent)"
+ label_sum: "Sum"
+ label_units: "Units"
+ label_week_reporting: "Week (Spent)"
+ label_year_reporting: "Year (Spent)"
+ load_query_question: "Report will have %{size} table cells and may take some time to render. Do you still want to try rendering it?"
+
+ permission_save_cost_reports: "Save public cost reports"
+ permission_save_private_cost_reports: "Save private cost reports"
+ project_module_reporting_module: "Cost reports"
+
+ text_costs_are_rounded_note: "Displayed values are rounded. All calculations are based on the non-rounded values."
+ toggle_multiselect: "activate/deactivate multiselect"
+
+ units: "Units"
+
+ validation_failure_date: "is not a valid date"
+ validation_failure_integer: "is not a valid integer"
diff --git a/vendored-plugins/openproject-reporting/config/locales/fr.yml b/vendored-plugins/openproject-reporting/config/locales/fr.yml
new file mode 100644
index 0000000000..3b5f468599
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/fr.yml
@@ -0,0 +1,49 @@
+fr:
+ button_save_as: Enregistrer le rapport sous…
+ comments: Commentaire
+ cost_reports_title: Rapports de coût
+ label_cost_report: Rapport de coût
+ description_drill_down: Afficher les détails
+ description_filter_selection: Sélection
+ description_multi_select: Voir la sélection multiple
+ description_remove_filter: Supprimer le filtre
+ information_restricted_depending_on_permission: Selon vos autorisations cette page peut contenir des informations restreintes.
+ label_click_to_edit: Cliquez pour modifier.
+ label_closed: fermé
+ label_columns: Colonnes
+ label_cost_entry_attributes: "Attributs de l'entrée de coût"
+ label_days_ago: au cours des derniers jours
+ label_entry: Entrée de coût
+ label_filter_text: Filtrer le texte
+ label_filter_value: Valeur
+ label_filters: Filtre
+ label_greater: '>'
+ label_is_not_project_with_subprojects: "n'est pas (y compris les sous-projets)"
+ label_is_project_with_subprojects: est (y compris les sous-projets)
+ label_work_package_attributes: Attributs du lot de travaux
+ label_less: '<'
+ label_money: Valeur marchande
+ label_month_reporting: Mois (passé)
+ label_new_report: Nouveau rapport de coût
+ label_open: ouvrir
+ label_operator: Opérateur
+ label_private_report_plural: Rapports privés de coûts
+ label_progress_bar_explanation: Rapport en cours de génération...
+ label_public_report_plural: Rapports publics de coûts
+ label_really_delete_question: Êtes-vous sûr de vouloir supprimer ce rapport ?
+ label_rows: Lignes
+ label_saving: Enregistrement en cours… ...
+ label_spent_on_reporting: Date (passée)
+ label_sum: Total
+ label_units: Unité
+ label_week_reporting: Semaine (passée)
+ label_year_reporting: Année (passée)
+ load_query_question: "Ce rapport a %{size} cellules et peut prendre un certain temps à s'afficher. Êtes-vous sûr de vouloir l'afficher ?"
+ permission_save_cost_reports: Enregistrer des rapports publics de coûts
+ permission_save_private_cost_reports: Enregistrer des rapports privés de coûts
+ project_module_reporting_module: Rapports de coût
+ text_costs_are_rounded_note: Les valeurs affichées sont arrondies. Tous les calculs sont basés sur les valeurs non arrondies.
+ toggle_multiselect: activer/désactiver la sélection multiple
+ units: Unité
+ validation_failure_date: "n'est pas une date valide"
+ validation_failure_integer: "n'est pas un nombre entier valide"
diff --git a/vendored-plugins/openproject-reporting/config/locales/id.yml b/vendored-plugins/openproject-reporting/config/locales/id.yml
new file mode 100644
index 0000000000..39699d763e
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/id.yml
@@ -0,0 +1,49 @@
+id:
+ button_save_as: Simpan sebagai...
+ comments: Komentar
+ cost_reports_title: Pelaporan biaya
+ label_cost_report: Laporan biaya
+ description_drill_down: Lihat detail
+ description_filter_selection: Seleksi
+ description_multi_select: Tampilkan multiselect
+ description_remove_filter: Hapus Filter
+ information_restricted_depending_on_permission: Informasi yang tersedia pada laman ini disesuaikan dengan hak akses anda.
+ label_click_to_edit: Klik untuk mengubah.
+ label_closed: berakhir
+ label_columns: Kolom
+ label_cost_entry_attributes: Atrribut masukan biaya
+ label_days_ago: beberapa hari terakhir
+ label_entry: Masukan biaya
+ label_filter_text: Filter teks
+ label_filter_value: Nilai
+ label_filters: Filter
+ label_greater: '>'
+ label_is_not_project_with_subprojects: yang bukan (termasuk dalam subproyek)
+ label_is_project_with_subprojects: adalah (termasuk dalam subproyek)
+ label_work_package_attributes: Attribut work package
+ label_less: '<'
+ label_money: Nilai uang
+ label_month_reporting: '#Bulan (Spent)'
+ label_new_report: Buat laporan biaya
+ label_open: terbuka
+ label_operator: Operator
+ label_private_report_plural: Laporan biaya terbatas
+ label_progress_bar_explanation: Membuat laporan...
+ label_public_report_plural: Laporan biaya umum
+ label_really_delete_question: Laporan akan dihapus, anda yakin?
+ label_rows: Baris
+ label_saving: Menyimpan ...
+ label_spent_on_reporting: Tanggal (spent)
+ label_sum: Total
+ label_units: Units
+ label_week_reporting: Minggu (spent)
+ label_year_reporting: Tahun (Spent)
+ load_query_question: 'Proses laporan memiliki %{size} cell dan akan memakan waktu yang relatif lama. Apakah anda ingin melanjutkan?'
+ permission_save_cost_reports: Simpan laporan biaya untuk umum
+ permission_save_private_cost_reports: Simpan laporan biaya untuk diri sendiri
+ project_module_reporting_module: Pelaporan biaya
+ text_costs_are_rounded_note: Tampilkan nilai pembulatan walaupun kalkulasi menggunakan nilai desimal.
+ toggle_multiselect: aktif/non-aktif multiselect
+ units: Units
+ validation_failure_date: validasi tanggal gagal
+ validation_failure_integer: validasi bilangan bulat gagal
diff --git a/vendored-plugins/openproject-reporting/config/locales/it.yml b/vendored-plugins/openproject-reporting/config/locales/it.yml
new file mode 100644
index 0000000000..0d5ef2a077
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/it.yml
@@ -0,0 +1,49 @@
+it:
+ button_save_as: Salvare il report come...
+ comments: Commento
+ cost_reports_title: Report costi
+ label_cost_report: Report costo
+ description_drill_down: Mostra dettagli
+ description_filter_selection: Selezione
+ description_multi_select: Visualizza multiselect
+ description_remove_filter: Rimuovi filtro
+ information_restricted_depending_on_permission: A seconda delle tue autorizzazioni, questa pagina visualizzare meno informazioni.
+ label_click_to_edit: Premi per modificare.
+ label_closed: chiuso
+ label_columns: Colonne
+ label_cost_entry_attributes: Attributi voce di costo
+ label_days_ago: durante gli ultimi giorni
+ label_entry: Voce di costo
+ label_filter_text: Filtro testo
+ label_filter_value: Valore
+ label_filters: Filtro
+ label_greater: '>'
+ label_is_not_project_with_subprojects: non è (include sottoprogetti)
+ label_is_project_with_subprojects: è (include sottoprogetti)
+ label_work_package_attributes: Attributi del pacchetto lavoro
+ label_less: '<'
+ label_money: Valore in contanti
+ label_month_reporting: Mese (passato)
+ label_new_report: Nuovo rapporto di costo
+ label_open: apri
+ label_operator: Operatore
+ label_private_report_plural: Report costo privato
+ label_progress_bar_explanation: Generazione Report...
+ label_public_report_plural: Report costo pubblico
+ label_really_delete_question: Sei sicuro di voler cancellare questo report?
+ label_rows: Righe
+ label_saving: Salvataggio in corso ...
+ label_spent_on_reporting: Data (passato)
+ label_sum: Somma
+ label_units: Unità di misura
+ label_week_reporting: Settimana (passato)
+ label_year_reporting: Anno (passato)
+ load_query_question: 'Il report avrà le celle della tabella del %{size} e potrebbe essere necessario del tempo per eseguire il rendering. Vuoi ancora tentare di eseguirne il rendering?'
+ permission_save_cost_reports: Salva il report costi pubblici
+ permission_save_private_cost_reports: Salvare il report costi privati
+ project_module_reporting_module: Report costi
+ text_costs_are_rounded_note: Valori visualizzati sono arrotondati. Tutti i calcoli sono basati sui valori non arrotondati.
+ toggle_multiselect: attivare/disattivare la multiselect
+ units: Unità di misura
+ validation_failure_date: non è una data valida
+ validation_failure_integer: non è un numero intero valido
diff --git a/vendored-plugins/openproject-reporting/config/locales/ja.yml b/vendored-plugins/openproject-reporting/config/locales/ja.yml
new file mode 100644
index 0000000000..b473d0551d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/ja.yml
@@ -0,0 +1,49 @@
+ja:
+ button_save_as: 名前を付けてレポートの保存
+ comments: コメント
+ cost_reports_title: コストレポート
+ label_cost_report: コストレポート
+ description_drill_down: 詳細を表示
+ description_filter_selection: 選択
+ description_multi_select: 複数選択
+ description_remove_filter: 絞り込みを外す
+ information_restricted_depending_on_permission: アクセス権に応じて、このページでは情報を制限されている場合があります。
+ label_click_to_edit: クリックして編集
+ label_closed: 完了
+ label_columns: 列
+ label_cost_entry_attributes: コストエントリの属性
+ label_days_ago: 直近の日の間
+ label_entry: コストエントリ
+ label_filter_text: テキストの絞り込み
+ label_filter_value: 値
+ label_filters: 絞り込み
+ label_greater: '>'
+ label_is_not_project_with_subprojects: ではない (子プロジェクトを含む)
+ label_is_project_with_subprojects: は(サブプロジェクトを含む)
+ label_work_package_attributes: 作業項目の属性
+ label_less: '<'
+ label_money: 金額
+ label_month_reporting: 月 (経過)
+ label_new_report: 新しいコストレポート
+ label_open: 未完了
+ label_operator: 演算子
+ label_private_report_plural: マイコストレポート
+ label_progress_bar_explanation: レポートを生成中...
+ label_public_report_plural: 共有コストレポート
+ label_really_delete_question: このレポートを削除してもよろしいですか?
+ label_rows: 行
+ label_saving: 保存中…
+ label_spent_on_reporting: 日付 (経過)
+ label_sum: 合計
+ label_units: 単位
+ label_week_reporting: 週 (経過)
+ label_year_reporting: 年 (経過)
+ load_query_question: 'レポートは%{size}つセルを持つために、描画するに時間がかかる場合があります。描画してもよろしいですか?'
+ permission_save_cost_reports: 共有コストレポートを保存
+ permission_save_private_cost_reports: マイコストレポートを保存
+ project_module_reporting_module: コストレポート
+ text_costs_are_rounded_note: 表示されている数字は四捨五入されています。すべての計算は非四捨五入の値に基づいています。
+ toggle_multiselect: 複数選択を有効/無効にする
+ units: 単位
+ validation_failure_date: は有効な日付ではありません。
+ validation_failure_integer: は有効な整数ではありません。
diff --git a/vendored-plugins/openproject-reporting/config/locales/pl.yml b/vendored-plugins/openproject-reporting/config/locales/pl.yml
new file mode 100644
index 0000000000..17061be9f1
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/pl.yml
@@ -0,0 +1,49 @@
+pl:
+ button_save_as: Zapisz raport jako...
+ comments: Komentarz
+ cost_reports_title: Raporty kosztów
+ label_cost_report: Raport kosztów
+ description_drill_down: Pokaż szczegóły
+ description_filter_selection: Wybór
+ description_multi_select: Pokaż multiselect
+ description_remove_filter: Usuń filtr
+ information_restricted_depending_on_permission: W zależności od uprawnień ta strona może zawierać ograniczone informacje.
+ label_click_to_edit: Kliknij, aby edytować.
+ label_closed: Zamknięte
+ label_columns: Kolumny
+ label_cost_entry_attributes: Wejściowe parametry kosztów
+ label_days_ago: podczas ostatnich dni
+ label_entry: Parametr kosztów
+ label_filter_text: Filtr tekstu
+ label_filter_value: Wartość
+ label_filters: Filtr
+ label_greater: '>'
+ label_is_not_project_with_subprojects: nie jest (zawiera podprojekty)
+ label_is_project_with_subprojects: jest (zawiera podprojekty)
+ label_work_package_attributes: Atrybuty zadań
+ label_less: '<'
+ label_money: Wartość pieniężna
+ label_month_reporting: Miesiąc (wydatki)
+ label_new_report: Nowy raport kosztowy
+ label_open: Otwarte
+ label_operator: Operator
+ label_private_report_plural: Prywatny raport kosztów
+ label_progress_bar_explanation: Generowanie raportu...
+ label_public_report_plural: Publiczny raport kosztów
+ label_really_delete_question: Na pewno chcesz usunąć ten raport?
+ label_rows: Wierszy
+ label_saving: Zapisywanie ...
+ label_spent_on_reporting: Data (wydatki)
+ label_sum: Suma
+ label_units: Jednostki
+ label_week_reporting: Tygodniu (wydatki)
+ label_year_reporting: Roku (wydatki)
+ load_query_question: 'Raport będzie mieć komórki tabeli %{size} i przygotowanie go może zająć trochę czasu. Czy chcesz uruchomić raport?'
+ permission_save_cost_reports: Zapisz publiczny raport kosztów
+ permission_save_private_cost_reports: Zapisz prywatny raport kosztów
+ project_module_reporting_module: Raporty kosztów
+ text_costs_are_rounded_note: Wyświetlane wartości są zaokrąglane. Wszystkie obliczenia opierają się na wartościach niezaokrąglonych.
+ toggle_multiselect: włączyć/wyłączyć multiselect
+ units: Jednostki
+ validation_failure_date: to nie jest poprawna data
+ validation_failure_integer: to nie jest prawidłową liczbą całkowitą
diff --git a/vendored-plugins/openproject-reporting/config/locales/pt-BR.yml b/vendored-plugins/openproject-reporting/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..a5f5f9c721
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/pt-BR.yml
@@ -0,0 +1,49 @@
+pt-BR:
+ button_save_as: Salvar relatório como...
+ comments: Comentário
+ cost_reports_title: Relatórios de custos
+ label_cost_report: Relatório de custos
+ description_drill_down: Exibir detalhes
+ description_filter_selection: Seleção
+ description_multi_select: Mostrar multiseleção
+ description_remove_filter: Remover filtro
+ information_restricted_depending_on_permission: Dependendo de suas permissões esta página pode conter informações restritas.
+ label_click_to_edit: Clique para editar.
+ label_closed: fechado
+ label_columns: Colunas
+ label_cost_entry_attributes: Atributos do custo de entrada
+ label_days_ago: durante os últimos dias
+ label_entry: Custo de entrada
+ label_filter_text: Filtro de texto
+ label_filter_value: Valor
+ label_filters: Filtro
+ label_greater: '>'
+ label_is_not_project_with_subprojects: não é (inclui subprojetos)
+ label_is_project_with_subprojects: é (inclui subprojetos)
+ label_work_package_attributes: Atributos do pacote de trabalho
+ label_less: '<'
+ label_money: Valor em dinheiro
+ label_month_reporting: Mês (gasto)
+ label_new_report: Novo relatório de custos
+ label_open: aberto
+ label_operator: Operador
+ label_private_report_plural: Relatórios privados de custos
+ label_progress_bar_explanation: Gerando relatório...
+ label_public_report_plural: Relatórios público de custos
+ label_really_delete_question: Tem certeza que deseja excluir este relatório?
+ label_rows: Linhas
+ label_saving: Salvando ...
+ label_spent_on_reporting: Data (gasto)
+ label_sum: Soma
+ label_units: Unidades
+ label_week_reporting: Semana (gasto)
+ label_year_reporting: Ano (gasto)
+ load_query_question: 'Relatório terá %{size} células da tabela e pode levar algum tempo para processar. Você ainda quer tentar processá-lo?'
+ permission_save_cost_reports: Salvar relatórios públicos de custos
+ permission_save_private_cost_reports: Salvar relatórios privados de custos
+ project_module_reporting_module: Relatórios de custos
+ text_costs_are_rounded_note: Valores exibidos são arredondados. Todos os cálculos baseiam-se em valores não-arredondados.
+ toggle_multiselect: ativar/desativar multiseleção
+ units: Unidades
+ validation_failure_date: não é uma data válida
+ validation_failure_integer: não é um valor inteiro válido
diff --git a/vendored-plugins/openproject-reporting/config/locales/ru.yml b/vendored-plugins/openproject-reporting/config/locales/ru.yml
new file mode 100644
index 0000000000..1ecb941c1d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/ru.yml
@@ -0,0 +1,49 @@
+ru:
+ button_save_as: Сохранить отчет как...
+ comments: Комментарий
+ cost_reports_title: Стоимость отчетов
+ label_cost_report: Стоимость отчета
+ description_drill_down: Показать в деталях
+ description_filter_selection: Выбор
+ description_multi_select: Показать множественный выбор
+ description_remove_filter: Убрать фильтрацию
+ information_restricted_depending_on_permission: В зависимости от вашего разрешения, эта страница может содержать закрытую информацию.
+ label_click_to_edit: Щелкнуть для изменения.
+ label_closed: закрыто
+ label_columns: Столбцы
+ label_cost_entry_attributes: Стоимость входных атрибутов
+ label_days_ago: в последние дни
+ label_entry: Стоимость входа
+ label_filter_text: Фильтровать текст
+ label_filter_value: Значение
+ label_filters: Фильтровать
+ label_greater: '>'
+ label_is_not_project_with_subprojects: нет (включая подпроекты)
+ label_is_project_with_subprojects: есть (включая подпроекты)
+ label_work_package_attributes: Атрибуты рабочего пакета
+ label_less: '<'
+ label_money: Денежная стоимость
+ label_month_reporting: Месяц (Потрачено)
+ label_new_report: Новый отчет о затратах
+ label_open: открыть
+ label_operator: Оператор
+ label_private_report_plural: Частный отчет о затратах
+ label_progress_bar_explanation: Создание отчета...
+ label_public_report_plural: Публичный отчет о затратах
+ label_really_delete_question: Вы действительно хотите удалить этот отчет?
+ label_rows: Строк
+ label_saving: Сохранение ...
+ label_spent_on_reporting: Дата (Потрачено)
+ label_sum: Сумма
+ label_units: Модули
+ label_week_reporting: Неделя (Потрачено)
+ label_year_reporting: Год (потрачено)
+ load_query_question: 'Отчет будет иметь таблицу из %{size} ячеек и может потребоваться некоторое время для его визуализации. Вы все еще хотите попробовать это?'
+ permission_save_cost_reports: Сохранить отчеты о публичных расходах
+ permission_save_private_cost_reports: Сохранить отчеты о частных расходах
+ project_module_reporting_module: Стоимость отчетов
+ text_costs_are_rounded_note: При отображении значения округляются. В расчетах используются неокругляемые значения.
+ toggle_multiselect: активировать/деактивировать множественный выбор
+ units: Модули
+ validation_failure_date: не является допустимой датой
+ validation_failure_integer: не является допустимым целым числом
diff --git a/vendored-plugins/openproject-reporting/config/locales/sv-SE.yml b/vendored-plugins/openproject-reporting/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..ce3fc7e5c6
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/locales/sv-SE.yml
@@ -0,0 +1,49 @@
+sv:
+ button_save_as: Spara rapporten som...
+ comments: Kommentar
+ cost_reports_title: Kostnadsrapporter
+ label_cost_report: Kostnadsrapport
+ description_drill_down: Visa detaljer
+ description_filter_selection: Urval
+ description_multi_select: Visa flerval
+ description_remove_filter: Ta bort filter
+ information_restricted_depending_on_permission: Beroende på dina behörigheter kan denna sida innehålla begränsad information.
+ label_click_to_edit: Klicka för att ändra.
+ label_closed: stängd
+ label_columns: Kolumner
+ label_cost_entry_attributes: Kostnadspostattribut
+ label_days_ago: under de senaste dagarna
+ label_entry: Kostnadspost
+ label_filter_text: Filtrera text
+ label_filter_value: Värde
+ label_filters: Filter
+ label_greater: '>'
+ label_is_not_project_with_subprojects: är inte (inkluderar delprojekt)
+ label_is_project_with_subprojects: är (inkluderar delprojekt)
+ label_work_package_attributes: Arbetspaketsattribut
+ label_less: '<'
+ label_money: Kontantvärde
+ label_month_reporting: Månad (spenderade)
+ label_new_report: Nya kostnadsrapport
+ label_open: öppen
+ label_operator: Operator
+ label_private_report_plural: Privata kostnadsrapporter
+ label_progress_bar_explanation: Genererar rapport...
+ label_public_report_plural: Offentliga kostnadsrapporter
+ label_really_delete_question: Är du säker på att du vill ta bort denna rapport?
+ label_rows: Rader
+ label_saving: Sparar ...
+ label_spent_on_reporting: Datum (spenderade)
+ label_sum: Summa
+ label_units: Enheter
+ label_week_reporting: Vecka (spenderade)
+ label_year_reporting: År (spenderade)
+ load_query_question: 'Rapporten kommer att ha %{size} celler och kan ta lite tid att skapa. Vill du prova att skapa den ändå?'
+ permission_save_cost_reports: Spara offentliga kostnadsrapporter
+ permission_save_private_cost_reports: Spara privata kostnadsrapporter
+ project_module_reporting_module: Kostnadsrapporter
+ text_costs_are_rounded_note: Visade värden är avrundade. Alla beräkningar är baserade på de icke-avrundade värdena.
+ toggle_multiselect: aktivera/avaktivera multival
+ units: Enheter
+ validation_failure_date: inte är ett giltigt datum
+ validation_failure_integer: är inte ett giltigt heltal
diff --git a/vendored-plugins/openproject-reporting/config/routes.rb b/vendored-plugins/openproject-reporting/config/routes.rb
new file mode 100644
index 0000000000..0fdc1130b8
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/config/routes.rb
@@ -0,0 +1,43 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+OpenProject::Application.routes.draw do
+ scope 'projects/:project_id' do
+ resources :cost_reports, except: :create do
+ collection do
+ match :index, via: [:get, :post]
+ end
+ end
+ end
+
+ resources :cost_reports, except: :create do
+ collection do
+ match :index, via: [:get, :post]
+ post :save_as, action: :create
+ get :drill_down
+ match :available_values, via: [:get, :post]
+ get :display_report_list
+ end
+
+ member do
+ post :update
+ post :rename
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/db/migrate/20110215143061_aggregated_reporting_migrations.rb b/vendored-plugins/openproject-reporting/db/migrate/20110215143061_aggregated_reporting_migrations.rb
new file mode 100644
index 0000000000..486d4ab892
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/db/migrate/20110215143061_aggregated_reporting_migrations.rb
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join("db","migrate","migration_utils","migration_squasher").to_s
+require 'open_project/plugins/migration_mapping'
+# This migration aggregates the migrations detailed in MIGRATION_FILES
+class AggregatedReportingMigrations < ActiveRecord::Migration
+
+ MIGRATION_FILES = <<-MIGRATIONS
+ 20101111150110_adjust_cost_query_layout.rb
+ 20101124150110_adjust_cost_query_layout_some_more.rb
+ 20101202142038_change_cost_query_yaml_length.rb
+ 20101203110501_rename_yamlized_to_serialized.rb
+ 20110215143010_add_timestamps_to_custom_fields.rb
+ MIGRATIONS
+
+ OLD_PLUGIN_NAME = "redmine_reporting"
+
+ def up
+ migration_names = OpenProject::Plugins::MigrationMapping.migration_files_to_migration_names(MIGRATION_FILES, OLD_PLUGIN_NAME)
+ Migration::MigrationSquasher.squash(migration_names) do
+ create_table "cost_queries" do |t|
+ t.integer "user_id", :null => false
+ t.integer "project_id"
+ t.string "name", :null => false
+ t.boolean "is_public", :default => false, :null => false
+ t.datetime "created_on", :null => false
+ t.datetime "updated_on", :null => false
+ t.string "serialized", :limit => 2000, :null => false
+ end
+
+ add_timestamps :custom_fields
+ end
+ end
+
+ def down
+ drop_table "cost_queries"
+ remove_timestamps :custom_fields
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/db/migrate/20130612104243_reporting_migrate_serialized_yaml.rb b/vendored-plugins/openproject-reporting/db/migrate/20130612104243_reporting_migrate_serialized_yaml.rb
new file mode 100644
index 0000000000..bd24b8e8e5
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/db/migrate/20130612104243_reporting_migrate_serialized_yaml.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require Rails.root.join("db","migrate","migration_utils","legacy_yamler").to_s
+
+class ReportingMigrateSerializedYaml < ActiveRecord::Migration
+ include Migration::LegacyYamler
+
+ def up
+ migrate_to_psych('cost_queries', 'serialized')
+ end
+
+ def down
+ migrate_to_psych('cost_queries', 'serialized')
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/db/migrate/20130925090243_cost_reports_migration.rb b/vendored-plugins/openproject-reporting/db/migrate/20130925090243_cost_reports_migration.rb
new file mode 100644
index 0000000000..14a29dcd45
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/db/migrate/20130925090243_cost_reports_migration.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class CostReportsMigration < ActiveRecord::Migration
+
+ class CostQuery < ActiveRecord::Base
+ serialize :serialized, Hash
+ end
+
+ def up
+ migrate_cost_queries({ "TrackerId" => "TypeId",
+ "IssueId" => "WorkPackageId" })
+ end
+
+ def down
+ migrate_cost_queries({ "TypeId" => "TrackerId",
+ "WorkPackageId" => "IssueId" })
+ end
+
+ private
+
+ def migrate_cost_queries(mapping)
+ CostQuery.find_each do |cost_query|
+ query = cost_query.serialized
+ [query[:filters], query[:group_bys]].each do |expression|
+ expression.each do |term|
+ attribute_mapping = mapping[term[0]]
+ term[0] = attribute_mapping if attribute_mapping
+ end
+ end
+ cost_query.save!
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/doc/CHANGELOG.md b/vendored-plugins/openproject-reporting/doc/CHANGELOG.md
new file mode 100644
index 0000000000..f995a6249d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/doc/CHANGELOG.md
@@ -0,0 +1,72 @@
+
+
+# Changelog
+
+## 3.0.8
+
+* `#7905` custom field not removed from cost report after delete
+* `#5191` Public Release Reporting Plugin
+* `#4024` Fix: Subpages have no unique page titles
+
+## 4.0.0
+
+* `#3216` Fix: Reporting creates 500 when a CustomField with content type version or user exists
+* `#4024` Subpages have no unique page titles
+
+## 4.0.0.pre9
+
+* Adaptations for new icon font
+* added icon for new project menu
+
+## 4.0.0.pre8
+
+* `#2731` Migrated serialized yaml from syck to psych
+
+## 4.0.0.pre7
+
+* `#2449` Squash old migrations
+* `#2517` XSS: unescaped fields in cost report view (cost log comment, work package title)
+
+## 4.0.0.pre6
+
+* New core version dependency.
+
+## 4.0.0.pre5
+
+* Rename IssueCategory to Category
+
+## 4.0.0.pre4
+
+* `#2061` Migrate to new data model.
+
+## 4.0.0.pre3
+
+Adapt to OpenProject core
+
+## 4.0.0.pre2
+
+* Fixed grouping by authors
+* Added reporting permissions
+* Translation fixes
+
+## 4.0.0.pre1
+
+* First release of the Rails 3 version
diff --git a/vendored-plugins/openproject-reporting/doc/COPYRIGHT.md b/vendored-plugins/openproject-reporting/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..de7d8de848
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+OpenProject Reporting Plugin
+
+An OpenProject plugin to create cost reports
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-reporting/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-reporting/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..812d9ca4fc
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/doc/COPYRIGHT_short.md
@@ -0,0 +1,16 @@
+OpenProject Reporting Plugin
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-reporting/doc/GPL.txt b/vendored-plugins/openproject-reporting/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-reporting/features/calculations.feature b/vendored-plugins/openproject-reporting/features/calculations.feature
new file mode 100644
index 0000000000..d76dc609ab
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/calculations.feature
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Cost Reporting Calculations
+
+ Scenario: Different Rates are calculated differently
+ Given there is a standard cost control project named "Cost Project"
+ And there is 1 hourly rate with the following:
+ | rate | 1 |
+ | user | manager |
+ | valid from | 1 year ago |
+ And there is 1 hourly rate with the following:
+ | rate | 5 |
+ | user | manager |
+ | valid from | 2 years ago |
+ And there is 1 default hourly rate with the following:
+ | rate | 10 |
+ | user | manager |
+ | valid from | 3 years ago |
+ And the project "Cost Project" has 1 time entry with the following:
+ | hours | 10 |
+ | user | manager |
+ | spent on | 6 months ago |
+ And the project "Cost Project" has 1 time entry with the following:
+ | hours | 10 |
+ | user | manager |
+ | spent on | 18 months ago |
+ And the project "Cost Project" has 1 time entry with the following:
+ | hours | 10 |
+ | user | manager |
+ | spent on | 30 months ago |
+ And there is 1 user with:
+ | login | admin |
+ | admin | true |
+ And I am already logged in as "admin"
+ And I am on the Cost Reports page for the project called "Cost Project" without filters or groups
+ Then I should see "10.00" # 1 EUR x 10 (hours)
+ And I should see "50.00" # 5 EUR x 10 (hours)
+ And I should see "100.00" # 10 EUR x 10 (hours)
+ And I should see "160.00"
diff --git a/vendored-plugins/openproject-reporting/features/filter.feature b/vendored-plugins/openproject-reporting/features/filter.feature
new file mode 100644
index 0000000000..0c1eaa9c9a
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/filter.feature
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Filter
+
+ @javascript
+ Scenario: We got some awesome default settings
+ Given there is a standard cost control project named "First Project"
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ Then filter "spent_on" should be visible
+ And filter "user_id" should be visible
+
+ @javascript
+ Scenario: A click on clear removes all filters
+ Given there is a standard cost control project named "First Project"
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ And I click on "Clear"
+ Then filter "spent_on" should not be visible
+ And filter "user_id" should not be visible
+
+ @javascript
+ Scenario: A set filter is getting restored after reload
+ Given there is a standard cost control project named "First Project"
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ And I click on "Clear"
+ And I set the filter "user_id" to the user with the login "developer" with the operator "!"
+ Then filter "user_id" should be visible
+ When I send the query
+ And the user with the login "developer" should be selected for "user_id_arg_1_val"
+ And "!" should be selected for "operators[user_id]"
+
diff --git a/vendored-plugins/openproject-reporting/features/grouping.feature b/vendored-plugins/openproject-reporting/features/grouping.feature
new file mode 100644
index 0000000000..906c1656de
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/grouping.feature
@@ -0,0 +1,84 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Groups
+
+ Background:
+ Given there is a standard cost control project named "First Project"
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+
+ @javascript
+ Scenario: We got some awesome default settings
+ Then I should see "Week (Spent)" in columns
+ And I should see "Work package" in rows
+
+ @javascript
+ Scenario: A click on clear removes all groups
+ When I click on "Clear"
+ Then I should not see "Week (Spent)" in columns
+ And I should not see "Work package" in rows
+ And I group rows by "User"
+ And I group rows by "Cost type"
+
+ When I click on "Clear"
+
+ Then I should not see "Week (Spent)" in columns
+ And I should not see "Work package" in rows
+ And I should not see "User" in rows
+ And I should not see "Cost type" in rows
+
+ @javascript
+ Scenario: Groups can be added to either rows or columns
+ When I click on "Clear"
+ And I group columns by "Work package"
+
+ Then I should see "Work package" in columns
+ When I group rows by "Project"
+ Then I should see "Project" in rows
+
+ @javascript
+ Scenario: Groups can be removed from rows and columns
+ When I click on "Clear"
+ And I group columns by "Work package"
+ And I group rows by "Project"
+
+ Then I should see "Work package" in columns
+ And I should see "Project" in rows
+
+ When I remove "Project" from rows
+ And I remove "Work package" from columns
+
+ Then I should not see "Work package" in columns
+ And I should not see "Project" in rows
+
+ @javascript
+ Scenario: Groups get restored after sending a query
+ When I click on "Clear"
+ And I group columns by "Work package"
+ And I group columns by "Project"
+ And I group rows by "User"
+ And I group rows by "Cost type"
+ And I send the query
+
+ Then I should see "Project" in columns
+ And I should see "Work package" in columns
+ And I should see "User" in rows
+ And I should see "Cost type" in rows
+
diff --git a/vendored-plugins/openproject-reporting/features/links.feature b/vendored-plugins/openproject-reporting/features/links.feature
new file mode 100644
index 0000000000..a71e0685e7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/links.feature
@@ -0,0 +1,139 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Cost Reporting Linkage
+
+ @javascript
+ Scenario: Coming to the cost report for the first time, I should see no entries that are not my own
+ Given there is a standard cost control project named "Some Project"
+ And there is 1 cost type with the following:
+ | name | Translation |
+ And the user "manager" has 1 cost entry
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "Some Project"
+
+ When I choose "Cash value"
+ And I send the query
+
+ Then I should see "User"
+ # And I should see "<< me >>"
+ # And I should see "me"
+ And I should see "No data to display"
+ And I should not see "0.00"
+
+ @javascript
+ Scenario: Coming to the cost report for the first time, I should see my entries
+ Given there is a standard cost control project named "Standard Project"
+ And the user "manager" has:
+ | hourly rate | 10 |
+ | default rate | 10 |
+ And the user "manager" has 1 issue with:
+ | subject | manager work_package |
+ And the issue "manager work_package" has 1 time entry with the following:
+ | user | manager |
+ | hours | 10 |
+ And there is 1 cost type with the following:
+ | name | word |
+ | cost rate | 1.01 |
+ And the work package "manager work_package" has 1 cost entry with the following:
+ | units | 7 |
+ | user | manager |
+ | cost type | word |
+ And I am already logged in as "manager"
+ And I am on the Cost Reports page for the project called "Standard Project"
+
+ When I choose "Cash value"
+ And I send the query
+
+ # 100 EUR (labour cost) + 7.07 EUR (words)
+ Then I should see "107.07"
+ And I should not see "No data to display"
+
+ #have to use annotation capybara due to
+ #https://github.com/aslakhellesoy/cucumber-rails/issues/issues/77
+ @javascript
+ Scenario: Going from an WorkPackage to the cost report should set the filter on this work_package
+ Given there is a standard cost control project named "Standard Project"
+ And the user "manager" has:
+ | default rate | 10 |
+ And the user "manager" has 1 issue with:
+ | subject | manager work_package |
+ And the user "manager" has 1 issue with:
+ | subject | another work_package |
+ And the issue "manager work_package" has 1 time entry with the following:
+ | user | manager |
+ | hours | 10 |
+ And the issue "another work_package" has 1 time entry with the following:
+ | user | manager |
+ | hours | 5 |
+ And I am already logged in as "manager"
+ And I am on the page for the issue "manager work_package"
+
+ Then I should see "10.00 hours"
+ When I follow "10.00 hours"
+
+ Then I should be on the Cost Reports page for the project called "Standard Project"
+ # 10 EUR x 10 (hours)
+ And I should see "100.00"
+ # 10 EUR x 5 (hours)
+ And I should not see "50.00"
+ And I should not see "150.00"
+
+ #have to use annotation capybara due to https://github.com/aslakhellesoy/cucumber-rails/issues/issues/77
+ @javascript
+ Scenario: Going from an WorkPackage to the cost report should set the filter on this work_package
+ Given there is a standard cost control project named "Standard Project"
+ And there is 1 cost type with the following:
+ | name | word |
+ | cost rate | 10 |
+ And the user "manager" has 1 issue with:
+ | subject | manager work_package |
+ And the user "manager" has 1 issue with:
+ | subject | another work_package |
+ And the work package "manager work_package" has 1 cost entry with the following:
+ | user | manager |
+ | units | 10 |
+ | cost type | word |
+ And the work package "another work_package" has 1 cost entry with the following:
+ | user | manager |
+ | units | 5 |
+ | cost type | word |
+ And I am already logged in as "manager"
+ And I am on the page for the work package "manager work_package"
+
+ Then I should see "10 words"
+
+ When I follow "10 words"
+
+ Then I should be on the Cost Reports page for the project called "Standard Project"
+ # 10 EUR x 10 (words)
+ And I should see "100.00"
+ # 10 EUR x 5 (words)
+ And I should not see "50.00"
+ And I should not see "150.00"
+
+ @javascript
+ Scenario: Jump to project from the cost report jumps to the cost report of the selected project
+ Given there is a standard cost control project named "First Project"
+ And there is a standard cost control project named "Second Project"
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ When I jump to project "Second Project"
+
+ Then I should be on the cost reports page of the project called "Second Project"
diff --git a/vendored-plugins/openproject-reporting/features/navigations.feature b/vendored-plugins/openproject-reporting/features/navigations.feature
new file mode 100644
index 0000000000..f473cd5a28
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/navigations.feature
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Navigating to reports page
+
+ Scenario: Navigating to the cost report of a project which is a subproject
+ Given there is 1 project with the following:
+ | name | ParentProject |
+ | identifier | parent_project_1 |
+ And the project "ParentProject" has 1 subproject with the following:
+ | name | SubProject |
+ | identifier | parent_project_1_sub_1 |
+ And there is 1 user with the following:
+ | login | bob |
+ And there is a role "Testrole"
+ And the role "Testrole" may have the following rights:
+ | view_cost_entries |
+ | view_own_cost_entries |
+ And the user "bob" is a "Testrole" in the project "SubProject"
+ When I login as "bob"
+ And I go to the page of the project called "SubProject"
+ And I follow "Cost reports" within "#main-menu"
+ Then I should see "Cost report" within "#content"
diff --git a/vendored-plugins/openproject-reporting/features/permission_variations.feature b/vendored-plugins/openproject-reporting/features/permission_variations.feature
new file mode 100644
index 0000000000..10d3c1a7c4
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/permission_variations.feature
@@ -0,0 +1,758 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Permissions
+######################
+# Dimensions to test:
+#
+# see_cost_entries: none, own, all
+# see_time_entries: none, own, all
+# see_rates: none, own, all
+
+ Scenario: Anonymous can not access the project specific cost reports page
+ Given there is a standard permission test project named "Permission_Test"
+ And I am not logged in
+ And I am on the Cost Reports page for the project called "Permission_Test" without filters or groups
+ Then I should see "Login"
+ And I should see "Password"
+
+ Scenario: Anonymous can not access the overall cost reports page as there are no other public projects
+ Given there is a standard permission test project named "Permission_Test"
+ And I am not logged in
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Login"
+ And I should see "Password"
+
+ @javascript
+ Scenario: Admin sees everything
+ Given there is a standard permission test project named "Permission_Test"
+ And there is 1 user with:
+ | login | admin |
+ | admin | true |
+ And I am already logged in as "admin"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I should not see "No data to display"
+ And I choose "Cash value"
+ And I click "Apply"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "11.11 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who has all rights sees everything
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "11.11 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ Scenario: User who has no rights, sees nothing
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | none |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ # permission denied
+ Then I should see "403"
+
+ @javascript
+ Scenario: User who may only see own cost entries, only sees his own cost entries without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see cost entries, sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see his own time entries, only sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see time entries, only sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see own time and cost entries, only sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see own time entries, but all cost entries, sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_time_entries |
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may only see own cost entries, but all time entries, sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who my see all time and cost entries, sees them without costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ Scenario: User who may see own costs, but no entries sees nothing
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ # access denied
+ Then I should see "403"
+
+ @javascript
+ Scenario: User who may see own costs and own cost entries, sees them with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see own costs and all cost entries, sees all cost entries, but own costs only
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should not see "11.11 EUR" within ".result"
+ And I should see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see own costs and own time entries, sees his entries with own costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_own_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: A user who may see own costs, own time entries and own cost entries, sees then with costs (as they are his costs)
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see own costs, own time entries and all cost entries, only sees those entries and only own entries with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_own_time_entries |
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see own costs and time entries, only sees own time entries with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who can see own costs, all time entries and only his own cost entries, see only the requested entries where costs are only visible on own entries
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_own_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see own costs and all entries, only sees his own entries attached with costs
+ # ATTENTION: there is no right to see own CostEntry costs - so no costs for cost entries are visible after all
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ Scenario: User who can see all costs but no entries sees nothing after all
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "403" #access denied
+
+ @javascript
+ Scenario: User wh can see all costs and his own cost entries, only sees own cost entries with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "1.00 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs and all cost entries, sees all cost entries with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "11.00 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should not see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs and own time entries, sees them with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs, own time- and cost- entries, sees his own entires with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "1.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs, own time entries and all cost entries, only sees them with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_cost_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "11.01 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should not see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs and all time entries, sees them with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "0.11 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should not see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
+
+ @javascript
+ Scenario: User who may see all costs, all time entries and his own cost entries, sees them with costs
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_cost_entries |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the overall Cost Reports page without filters or groups
+ Then I should see "Cost Report" within "#content"
+ And I choose "Cash value"
+ And I click "Apply"
+ And I should not see "No data to display"
+ # Costs
+ # costs (0.01 [own, time] + 0.10 [other, time] + 1.00 [own, cost] + 10.00 [other, cost])
+ And I should see "1.11 EUR" within ".result"
+ And I should not see "-" within ".result"
+ # TimeEntries
+ # own
+ And I should see "1.00 hour"
+ # other
+ And I should see "2.00 hour"
+ # CostEntries
+ # own
+ And I should see "1.0 one"
+ # other
+ And I should not see "1.0 ten"
diff --git a/vendored-plugins/openproject-reporting/features/saving.feature b/vendored-plugins/openproject-reporting/features/saving.feature
new file mode 100644
index 0000000000..25e17bfb30
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/saving.feature
@@ -0,0 +1,132 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Feature: Saving Queries
+
+ @javascript
+ Scenario: Reports can be saved as private
+ Given there is a standard cost control project named "First Project"
+ And the role "Controller" may have the following rights:
+ | view_own_hourly_rate |
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_entries |
+ | view_time_entries |
+ | save_cost_reports |
+ | save_private_cost_reports |
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ Then I should see "Save" within "#query-icon-save-as"
+ And I click on "Clear"
+ And I group columns by "Work package"
+ And I group rows by "Project"
+ And I set the filter "user_id" to the user with the login "developer" with the operator "!"
+ And I click on "Save"
+ And I fill in "Testreport" for "query_name"
+ And I click on "Save" within "#save_as_form"
+ Then I should see "Testreport" within "#ur_caption"
+ And I should see "Testreport" within "#private_sidebar_report_list"
+ And I should see "Work package" in columns
+ And I should see "Project" in rows
+ And filter "user_id" should be visible
+
+ @javascript
+ Scenario: Reports can be saved as public
+ Given there is a standard cost control project named "First Project"
+ And the role "Controller" may have the following rights:
+ | view_own_hourly_rate |
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_entries |
+ | view_time_entries |
+ | save_cost_reports |
+ | save_private_cost_reports |
+ And I am already logged in as "controller"
+ And I am on the Cost Reports page for the project called "First Project"
+ Then I should see "Save" within "#query-icon-save-as"
+ And I click on "Clear"
+ And I group columns by "Work package"
+ And I group rows by "Project"
+ And I set the filter "user_id" to the user with the login "developer" with the operator "!"
+ And I click on "Save"
+ And I fill in "Testreport" for "query_name"
+ And I check "Public"
+ And I click on "Save" within "#save_as_form"
+ Then I should see "Testreport" within "#ur_caption"
+ And I should see "Testreport" within "#public_sidebar_report_list"
+ And I should see "Work package" in columns
+ And I should see "Project" in rows
+ And filter "user_id" should be visible
+
+ @javascript
+ Scenario: Reports can't be saved by users without permissions
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_time_entries |
+ And I am already logged in as "testuser"
+ And I am on the Cost Reports page for the project called "Permission_Test"
+ Then I should not see "Save" within ".buttons"
+
+ @javascript
+ Scenario: Public Reports can't be saved by users allowed to save private queries
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_time_entries |
+ | save_private_cost_reports |
+ And I am already logged in as "testuser"
+ And I am on the Cost Reports page for the project called "Permission_Test"
+ Then I should see "Save"
+ And I click on "Save"
+ Then I should not see "Public"
+ And I fill in "Testreport" for "query_name"
+ And I click on "Save" within "#save_as_form"
+ Then I should see "Testreport" within "#ur_caption"
+ And I should see "Testreport" within "#private_sidebar_report_list"
+
+ @javascript
+ Scenario: Users that can save cost reports can save either public or private
+ Given there is a standard permission test project named "Permission_Test"
+ And the role "Testuser" may have the following rights:
+ | view_hourly_rates |
+ | view_time_entries |
+ | save_cost_reports |
+ And I am already logged in as "testuser"
+ And I am on the Cost Reports page for the project called "Permission_Test"
+ Then I should see "Save" within "#query-icon-save-as"
+ And I click on "Save"
+ Then I should see "Public"
+ And I fill in "Testreport" for "query_name"
+ And I click on "Save" within "#save_as_form"
+ Then I should see "Testreport" within "#ur_caption"
+ And I should see "Testreport" within "#private_sidebar_report_list"
+ Then I should see "Save" within "#query-icon-save-as"
+ And I click on "Save report as..."
+ Then I should see "Public"
+ And I check "Public"
+ And I fill in "Testreport2" for "query_name"
+ And I follow "Save" within "#save_as_form"
+ Then I should see "Testreport2" within "#ur_caption"
+ And I should see "Testreport2" within "#public_sidebar_report_list"
+ And I should see "Testreport" within "#private_sidebar_report_list"
diff --git a/vendored-plugins/openproject-reporting/features/step_definitions/custom_steps.rb b/vendored-plugins/openproject-reporting/features/step_definitions/custom_steps.rb
new file mode 100644
index 0000000000..7f71959a7a
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/step_definitions/custom_steps.rb
@@ -0,0 +1,141 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+Given /^there is a standard permission test project named "([^\"]*)"$/ do |name|
+ steps %Q{
+ Given there is 1 project with the following:
+ | Name | #{name} |
+ And the project "#{name}" has the following types:
+ | name | position |
+ | Bug | 1 |
+ And the project "#{name}" has 1 issue with:
+ | subject | #{name}work_package |
+ And there is a role "Testuser"
+ And the role "Testuser" may have the following rights:
+ | view_own_hourly_rate |
+ | view_hourly_rates |
+ | view_cost_rates |
+ | view_own_time_entries |
+ | view_own_cost_entries |
+ | view_cost_entries |
+ | view_time_entries |
+ And there is 1 User with:
+ | Login | testuser |
+ | Firstname | Test |
+ | Lastname | User |
+ | default rate | 0.01 |
+ And the user "testuser" is a "Testuser" in the project "#{name}"
+ And there is 1 User with:
+ | Login | otheruser |
+ | Firstname | Other |
+ | Lastname | User |
+ | default rate | 0.05 |
+ And the user "otheruser" is a "Testuser" in the project "#{name}"
+ And there is 1 cost type with the following:
+ | name | one |
+ | cost rate | 1.00 |
+ And there is 1 cost type with the following:
+ | name | ten |
+ | cost rate | 10.00 |
+ And the issue "#{name}work_package" has 1 time entry with the following:
+ | hours | 1 |
+ | user | testuser |
+ And the issue "#{name}work_package" has 1 time entry with the following:
+ | hours | 2 |
+ | user | otheruser |
+ And the work package "#{name}work_package" has 1 cost entry with the following:
+ | units | 1 |
+ | user | testuser |
+ | cost type | one |
+ And the work package "#{name}work_package" has 1 cost entry with the following:
+ | units | 1 |
+ | user | otheruser |
+ | cost type | ten |
+ }
+end
+
+Given /^I set the filter "([^\"]*)" to "([^\"]*)" with the operator "([^\"]*)"$/ do |filter, value, operator|
+ begin
+ find_by_id("add_filter_select").find("[value='#{filter}']").select_option
+ find("[name='operators[#{filter}]']").find("[value='#{operator}']").select_option
+ find("[name='values[#{filter}][]']").find("[value='#{value}']").select_option
+ rescue Capybara::ElementNotFound
+ # we support both using all-values and all-texts parameters
+ step %{I select "#{filter}" from "add_filter_select"}
+
+ filter_id = find_by_id("add_filter_select").value
+ step %{I select "#{operator}" from "operators[#{filter_id}]"}
+ step %{I select "#{value}" from "#{filter_id}_arg_1_val"}
+ end
+end
+
+Given /^I set the filter "([^\"]*)" to the user with the login "([^\"]*)" with the operator "([^\"]*)"$/ do |filter, login, operator|
+ user_id = User.find_by_login(login).id
+ step %{I set the filter "#{filter}" to "#{user_id}" with the operator "#{operator}"}
+end
+
+When /the user with the login "([^\"]*)" should be selected for "([^\"]*)"/ do |login, select_id|
+ user_id = User.find_by_login(login).id
+ step %{"#{user_id}" should be selected for "#{select_id}"}
+end
+
+When /^I send the query$/ do
+ find("[id='query-icon-apply-button']").click
+end
+
+Then /^filter "([^\"]*)" should (not )?be visible$/ do |filter, negative|
+ bool = negative ? false : true
+ page.evaluate_script("$('filter_#{filter}').visible()") =~ /^#{bool}$/
+end
+
+Then /^(?:|I )should not see "([^\"]*)" in (columns|rows)$/ do |text, axis|
+ page.all("fieldset#group_by_#{axis} span").each do |element|
+ element.should_not have_content(text)
+ end
+end
+
+Then /^(?:|I )should see "([^\"]*)" in (columns|rows)$/ do |text, axis|
+ page.should have_xpath(".//fieldset[@id='group_by_#{axis}']/span[contains(label,'#{text}')]")
+end
+
+Given /^I group (rows|columns) by "([^\"]*)"/ do |target, group|
+ step %{I select "#{group}" from "add_group_by_#{target}" within "#group_by_#{target}"}
+end
+
+Given /^I remove "([^\"]*)" from (rows|columns)/ do |group, source|
+ element_name = find_by_id("group_by_#{source}").find("label", text: "#{group}")[:for]
+ find_by_id("#{element_name}_remove").click
+end
+
+Given /^I (delete|remove) the (cost|time) entry "([^\"]*)"$/ do |method, type, name|
+ begin
+ find(:xpath, "//tr[td=#{name}]/td/a[@title='Delete']").click
+ rescue Capybara::ElementNotFound
+ find(:xpath, "//tr[td[@raw-data='#{name}']]/td/a[@title='Delete']").click
+ end
+ step %{I accept the alert dialog}
+end
+
+Given /^I edit the report name in place$/ do
+ find(:css, "#query_saved_name").click
+end
+
+Then /^I see spent time$/ do
+ page.should have_content(/spent time/i)
+end
diff --git a/vendored-plugins/openproject-reporting/features/step_definitions/disabled_scenarios.rb b/vendored-plugins/openproject-reporting/features/step_definitions/disabled_scenarios.rb
new file mode 100644
index 0000000000..e9b59c08f7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/features/step_definitions/disabled_scenarios.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+ScenarioDisabler.disable(feature: "Tracking Time", scenario: "Adding a time entry")
+ScenarioDisabler.disable(feature: "Tracking Time", scenario: "Editing a time entry")
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting.rb
new file mode 100644
index 0000000000..7813655f43
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module Reporting
+ require "open_project/reporting/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/default_data.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/default_data.rb
new file mode 100644
index 0000000000..be7cbe4f6f
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/default_data.rb
@@ -0,0 +1,32 @@
+module OpenProject
+ module Reporting
+ module DefaultData
+ module_function
+
+ def load!
+ restrict_project_admin_permissions!
+ end
+
+ ##
+ # The Project Admin role is assigned all possible permission in
+ # the core's `roles.rb` seed. Here we remove those permissions
+ # which should not be assigned by default.
+ def restrict_project_admin_permissions!
+ role = project_admin_role or raise 'Project admin role not found'
+
+ role.remove_permission! *restricted_project_admin_permissions
+ end
+
+ def project_admin_role
+ Role.find_by name: I18n.t(:default_role_project_admin)
+ end
+
+ def restricted_project_admin_permissions
+ [
+ :save_cost_reports,
+ :save_private_cost_reports
+ ]
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/engine.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/engine.rb
new file mode 100644
index 0000000000..5311292cd5
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/engine.rb
@@ -0,0 +1,106 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting
+ class Engine < ::Rails::Engine
+ engine_name :openproject_reporting
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-reporting',
+ author_url: 'http://finn.de',
+ requires_openproject: '>= 4.0.0' do
+
+ view_actions = [:index, :show, :drill_down, :available_values, :display_report_list]
+ edit_actions = [:create, :update, :rename, :delete]
+
+ #register reporting_module including permissions
+ project_module :reporting_module do
+ permission :save_cost_reports, {cost_reports: edit_actions}
+ permission :save_private_cost_reports, {cost_reports: edit_actions}
+ end
+
+ #register additional permissions for viewing time and cost entries through the CostReportsController
+ view_actions.each do |action|
+ Redmine::AccessControl.permission(:view_time_entries).actions << "cost_reports/#{action}"
+ Redmine::AccessControl.permission(:view_own_time_entries).actions << "cost_reports/#{action}"
+ Redmine::AccessControl.permission(:view_cost_entries).actions << "cost_reports/#{action}"
+ Redmine::AccessControl.permission(:view_own_cost_entries).actions << "cost_reports/#{action}"
+ end
+
+ #menu extensions
+ menu :top_menu, :cost_reports_global, {controller: '/cost_reports', action: 'index', project_id: nil},
+ caption: :cost_reports_title,
+ if: Proc.new {
+ (User.current.logged? || !Setting.login_required?) &&
+ (
+ User.current.allowed_to?(:view_time_entries, nil, global: true) ||
+ User.current.allowed_to?(:view_own_time_entries, nil, global: true) ||
+ User.current.allowed_to?(:view_cost_entries, nil, global: true) ||
+ User.current.allowed_to?(:view_own_cost_entries, nil, global: true)
+ )
+ }
+
+ menu :project_menu, :cost_reports,
+ {controller: '/cost_reports', action: 'index'},
+ param: :project_id,
+ after: :time_entries,
+ caption: :cost_reports_title,
+ if: Proc.new { |project| project.module_enabled?(:reporting_module) },
+ html: { class: 'icon2 icon-cost-reports' }
+
+ # Cost reports should remove the default time entries menu item
+ hide_menu_item :project_menu,
+ :time_entries,
+ hide_if: -> (project) { project.module_enabled?(:reporting_module) }
+
+ hide_menu_item :top_menu,
+ :time_sheet
+ end
+
+ initializer "reporting.register_hooks" do
+ # don't use require_dependency to not reload hooks in development mode
+ require 'open_project/reporting/hooks'
+ end
+
+ initializer 'reporting.precompile_assets' do
+ Rails.application.config.assets.precompile += %w(
+ reporting_engine/reporting_engine.css
+ reporting_engine/reporting_engine.js
+ )
+ end
+
+ config.to_prepare do
+ require_dependency 'report/walker'
+ require_dependency 'report/transformer'
+ require_dependency 'widget/sortable_init'
+ require_dependency 'widget/simple_table'
+ require_dependency 'widget/entry_table'
+ require_dependency 'widget/settings_patch'
+ require_dependency 'cost_query/group_by'
+ end
+
+ patches [:CostlogController,
+ :TimelogController,
+ :CustomFieldsController,
+ :'OpenProject::Configuration']
+ patch_with_namespace :BasicData, :RoleSeeder
+ patch_with_namespace :BasicData, :SettingSeeder
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/hooks.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/hooks.rb
new file mode 100644
index 0000000000..01f10b9ef1
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/hooks.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::Hooks
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches.rb
new file mode 100644
index 0000000000..a7a88113f9
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::Patches
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/costlog_controller_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/costlog_controller_patch.rb
new file mode 100644
index 0000000000..3c940c853e
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/costlog_controller_patch.rb
@@ -0,0 +1,92 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'costlog_controller'
+
+module OpenProject::Reporting::Patches
+ module CostlogControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+
+ alias_method_chain :index, :reports_view
+ alias_method_chain :find_optional_project, :own
+ end
+ end
+
+ module InstanceMethods
+
+ ##
+ # @Override
+ # This is for cost reporting
+ def redirect_to(*args, &block)
+ if args.first == :back and args.size == 1 and request.referer =~ /cost_reports/
+ super(controller: '/cost_reports', action: :index)
+ else
+ super(*args, &block)
+ end
+ end
+
+ def index_with_reports_view
+ # we handle single project reporting currently
+ if @project.nil? || !@project.module_enabled?(:reporting_module)
+ return index_without_reports_view
+ end
+ filters = {operators: {}, values: {}}
+
+ if @work_package
+ if @work_package.respond_to?("lft")
+ work_package_ids = @work_package.self_and_descendants.pluck(:id)
+ else
+ work_package_ids = [@work_package.id]
+ end
+
+ filters[:operators][:work_package_id] = "="
+ filters[:values][:work_package_id] = work_package_ids
+ end
+
+ filters[:operators][:project_id] = "="
+ filters[:values][:project_id] = [@project.id.to_s]
+
+ respond_to do |format|
+ format.html {
+ session[CostQuery.name.underscore.to_sym] = { filters: filters, groups: {rows: [], columns: []} }
+
+ if @cost_type
+ redirect_to controller: "/cost_reports", action: "index", project_id: @project, unit: @cost_type.id
+ else
+ redirect_to controller: "/cost_reports", action: "index", project_id: @project
+ end
+ return
+ }
+ format.all {
+ index_without_report_view
+ }
+ end
+ end
+
+ def find_optional_project_with_own
+ find_optional_project_without_own
+ deny_access unless User.current.allowed_to?(:view_cost_entries, @project, global: true) ||
+ User.current.allowed_to?(:view_own_cost_entries, @project, global: true)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/custom_fields_controller_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/custom_fields_controller_patch.rb
new file mode 100644
index 0000000000..d595737902
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/custom_fields_controller_patch.rb
@@ -0,0 +1,86 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'costlog_controller'
+
+module OpenProject::Reporting::Patches
+ module CustomFieldsControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+
+ alias_method_chain :destroy, :custom_fields
+ end
+ end
+
+ module InstanceMethods
+
+ def destroy_with_custom_fields
+ id = @custom_field.id
+
+ reports = CostQuery.where("serialized LIKE '%CustomField#{id}%'")
+
+ remove_custom_field_from_cost_report(reports, id)
+ remove_custom_field_from_session(id)
+
+ destroy_without_custom_fields
+ end
+
+ private
+
+ def remove_custom_field_from_cost_report(affected_reports, id)
+ custom_field_name = "CustomField#{id}"
+
+ affected_reports.each do |report|
+ filters = reject_from_query_properties(report, :filters, custom_field_name)
+ group_bys = reject_from_query_properties(report, :group_bys, custom_field_name)
+ updated_report = build_query(report.engine, filters, group_bys)
+ report.migrate(updated_report)
+ report.save!
+ end
+ end
+
+ def reject_from_query_properties(report, property, custom_field_name)
+ report.serialized[property].reject { |f| f[0] == custom_field_name }
+ end
+
+ def build_query(report_engine, filters, groups = {})
+ query = report_engine.deserialize({ filters: filters, group_bys: groups })
+ query.serialize
+ query
+ end
+
+ def remove_custom_field_from_session(id)
+ custom_field_name = "custom_field#{id}".to_sym
+ report_engine_name = CostQuery.name.underscore.to_sym
+ cookie = session[report_engine_name]
+
+ if cookie
+ cookie[:filters][:operators].delete(custom_field_name)
+ cookie[:filters][:values].delete(custom_field_name)
+ cookie[:groups][:rows].delete(custom_field_name.to_s)
+ cookie[:groups][:columns].delete(custom_field_name.to_s)
+
+ session[report_engine_name] = cookie
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/open_project/configuration_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/open_project/configuration_patch.rb
new file mode 100644
index 0000000000..4497c3a636
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/open_project/configuration_patch.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+
+require_dependency 'open_project/configuration'
+
+module OpenProject::Reporting::Patches
+ module OpenProject::ConfigurationPatch
+ def self.included(base)
+ base.class_eval do
+ extend ModuleMethods
+
+ @defaults['cost_reporting_cache_filter_classes'] = true
+
+ if config_loaded_before_patch?
+ @config['cost_reporting_cache_filter_classes'] = true
+ end
+ end
+ end
+
+ module ModuleMethods
+ def config_loaded_before_patch?
+ @config.present? && !@config.has_key?('cost_reporting_cache_filter_classes')
+ end
+
+ def cost_reporting_cache_filter_classes
+ @config['cost_reporting_cache_filter_classes']
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/role_seeder_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/role_seeder_patch.rb
new file mode 100644
index 0000000000..4d6ea6bc8d
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/role_seeder_patch.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::Patches::RoleSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def seed_data!
+ super.tap do |_|
+ OpenProject::Reporting::DefaultData.load!
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/setting_seeder_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/setting_seeder_patch.rb
new file mode 100644
index 0000000000..09e19b5eb1
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/setting_seeder_patch.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Costs Plugin
+#
+# Copyright (C) 2009 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::Patches::SettingSeederPatch
+ def self.included(base) # :nodoc:
+ base.prepend InstanceMethods
+ end
+
+ module InstanceMethods
+ def data
+ original_data = super
+
+ unless original_data['default_projects_modules'].include? 'reporting_module'
+ original_data['default_projects_modules'] << 'reporting_module'
+ end
+
+ original_data
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/timelog_controller_patch.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/timelog_controller_patch.rb
new file mode 100644
index 0000000000..286d314629
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/patches/timelog_controller_patch.rb
@@ -0,0 +1,92 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'timelog_controller'
+
+module OpenProject::Reporting::Patches
+ module TimelogControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+
+ alias_method_chain :index, :reports_view
+ alias_method_chain :find_optional_project, :own
+ end
+ end
+
+ module InstanceMethods
+
+ ##
+ # @Override
+ # This is for cost reporting
+ def redirect_to(*args, &block)
+ if args.first == :back and args.size == 1 and request.referer =~ /cost_reports/
+ super(controller: '/cost_reports', action: :index)
+ else
+ super(*args, &block)
+ end
+ end
+
+ def index_with_reports_view
+ # we handle single project reporting currently
+ if @project.nil? || !@project.module_enabled?(:reporting_module)
+ return index_without_reports_view
+ end
+ filters = {operators: {}, values: {}}
+
+ if @issue
+ if @issue.respond_to?("lft")
+ work_package_ids = @issue.self_and_descendants.pluck(:id)
+ else
+ work_package_ids = [@issue.id.to_s]
+ end
+
+ filters[:operators][:work_package_id] = "="
+ filters[:values][:work_package_id] = [work_package_ids]
+ end
+
+ filters[:operators][:project_id] = "="
+ filters[:values][:project_id] = [@project.id.to_s]
+
+ respond_to do |format|
+ format.html {
+ session[::CostQuery.name.underscore.to_sym] = { filters: filters, groups: {rows: [], columns: []} }
+
+ redirect_to controller: "/cost_reports", action: "index", project_id: @project, unit: -1
+ }
+ format.all {
+ index_without_report_view
+ }
+ end
+ end
+
+ def find_optional_project_with_own
+ if !params[:work_package_id].blank?
+ @issue = WorkPackage.find(params[:work_package_id])
+ @project = @issue.project
+ elsif !params[:project_id].blank?
+ @project = Project.find(params[:project_id])
+ end
+ deny_access unless User.current.allowed_to?(:view_time_entries, @project, global: true) ||
+ User.current.allowed_to?(:view_own_time_entries, @project, global: true)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/open_project/reporting/version.rb b/vendored-plugins/openproject-reporting/lib/open_project/reporting/version.rb
new file mode 100644
index 0000000000..f2aa80b4f7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/open_project/reporting/version.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject
+ module Reporting
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/openproject-reporting.rb b/vendored-plugins/openproject-reporting/lib/openproject-reporting.rb
new file mode 100644
index 0000000000..5fbab2db54
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/openproject-reporting.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'open_project/reporting'
diff --git a/vendored-plugins/openproject-reporting/lib/widget/cost_types.rb b/vendored-plugins/openproject-reporting/lib/widget/cost_types.rb
new file mode 100644
index 0000000000..6e7b0bee25
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/widget/cost_types.rb
@@ -0,0 +1,41 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::CostTypes < Widget::Base
+ def render_with_options(options, &block)
+ @cost_types = options.delete(:cost_types)
+ @selected_type_id = options.delete(:selected_type_id)
+
+ super(options, &block)
+ end
+
+ def render
+ write contents
+ end
+
+ def contents
+ content_tag :div do
+ available_cost_type_tabs(@subject).sort_by { |id, _| id }.map do |id, label|
+ type_selection = radio_button_tag('unit', id, id == @selected_type_id)
+ type_selection += label_tag "unit_#{id}", h(label)
+ type_selection
+ end.join(' ').html_safe
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/widget/entry_table.rb b/vendored-plugins/openproject-reporting/lib/widget/entry_table.rb
new file mode 100644
index 0000000000..e54e85ac8a
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/widget/entry_table.rb
@@ -0,0 +1,168 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table::EntryTable < Widget::Table
+ FIELDS = [:spent_on, :user_id, :activity_id, :work_package_id, :comments, :project_id]
+
+ detailed_table self
+
+ def render
+ content = content_tag :div, class: 'generic-table--container -with-footer' do
+ content_tag :div, class: 'generic-table--results-container' do
+ table = content_tag :table, 'interactive-table' => true,
+ role: 'grid',
+ class: 'generic-table',
+ id: 'sortable-table' do
+ concat colgroup
+ concat head
+ concat foot
+ concat body
+ end
+ table +
+ content_tag(:div, class: 'generic-table--header-background') {} +
+ content_tag(:div, class: 'generic-table--footer-background') {}
+ end
+ end
+ # FIXME do that js-only, like a man's man
+ render_widget Widget::Table::SortableInit, @subject, to: content, sort_first_row: true
+ write content
+ end
+
+ def colgroup
+ content_tag :colgroup do
+ FIELDS.each do
+ concat content_tag(:col, 'highlight-col' => true) {}
+ end
+ concat content_tag(:col, 'highlight-col' => true) {}
+ concat content_tag(:col, 'highlight-col' => true) {}
+ concat content_tag(:col) {}
+ end
+ end
+
+ def head
+ content_tag :thead do
+ content_tag :tr do
+ FIELDS.map do |field|
+ concat content_tag(:th) {
+ content_tag(:div, class: 'generic-table--sort-header-outer') do
+ content_tag(:div, class: 'generic-table--sort-header') do
+ content_tag(:span, label_for(field))
+ end
+ end
+ }
+ end
+ concat content_tag(:th) {
+ content_tag(:div, class: 'generic-table--sort-header-outer') do
+ content_tag(:div, class: 'generic-table--sort-header') do
+ content_tag(:span, cost_type.try(:unit_plural) || l(:units))
+ end
+ end
+ }
+ concat content_tag(:th) {
+ content_tag(:div, class: 'generic-table--sort-header-outer') do
+ content_tag(:div, class: 'generic-table--sort-header') do
+ content_tag(:span, CostEntry.human_attribute_name(:costs))
+ end
+ end
+ }
+ hit = false
+ @subject.each_direct_result do |result|
+ next if hit
+ if entry_for(result).editable_by? User.current
+ concat content_tag(:th, class: 'unsortable') {}
+ hit = true
+ end
+ end
+ end
+ end
+ end
+
+ def foot
+ content_tag :tfoot do
+ content_tag :tr do
+ if show_result(@subject, 0) != show_result(@subject)
+ concat content_tag(:th, '', colspan: FIELDS.size)
+ concat content_tag(:th) {
+ concat content_tag(:div,
+ show_result(@subject),
+ class: 'inner generic-table--footer-outer')
+ }
+ concat content_tag(:th) {
+ concat content_tag(:div,
+ show_result(@subject, 0),
+ class: 'result generic-table--footer-outer')
+ }
+ else
+ concat content_tag(:th, '', colspan: FIELDS.size + 1)
+ concat content_tag(:th) {
+ concat content_tag(:div,
+ show_result(@subject),
+ class: 'result generic-table--footer-outer')
+ }
+ end
+ concat content_tag(:th, '', class: 'unsortable')
+ end
+ end
+ end
+
+ def body
+ content_tag :tbody do
+ rows = ''.html_safe
+ @subject.each_direct_result do |result|
+ rows << (content_tag(:tr) do
+ ''.html_safe
+ FIELDS.each do |field|
+ concat content_tag(:td, show_field(field, result.fields[field.to_s]).html_safe,
+ :'raw-data' => raw_field(field, result.fields[field.to_s]),
+ class: 'left')
+ end
+ concat content_tag :td, show_result(result, result.fields['cost_type_id'].to_i).html_safe,
+ class: 'units right',
+ :'raw-data' => result.units
+ concat content_tag :td,
+ (show_result(result, 0)).html_safe,
+ class: 'currency right',
+ :'raw-data' => result.real_costs
+ concat content_tag :td, icons(result)
+ end)
+ end
+ rows
+ end
+ end
+
+ def icons(result)
+ icons = ''
+ with_project(result.fields['project_id']) do
+ if entry_for(result).editable_by? User.current
+ icons = link_to(icon_wrapper('icon-context icon-edit', l(:button_edit)),
+ action_for(result, action: 'edit'),
+ class: 'no-decoration-on-hover',
+ title: l(:button_edit))
+ icons << link_to(icon_wrapper('icon-context icon-delete', l(:button_delete)),
+ (action_for(result, action: 'destroy')
+ .reverse_merge(authenticity_token: form_authenticity_token)),
+ data: { confirm: l(:text_are_you_sure) },
+ method: :delete,
+ class: 'no-decoration-on-hover',
+ title: l(:button_delete))
+ end
+ end
+ icons
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/widget/settings_patch.rb b/vendored-plugins/openproject-reporting/lib/widget/settings_patch.rb
new file mode 100644
index 0000000000..6641833358
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/widget/settings_patch.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+#explicitly require what will be patched to be loaded from the ReportingEngine
+require_dependency 'widget/settings'
+class Widget::Settings < Widget::Base
+ @@settings_to_render.insert -2, :cost_types
+
+ def render_cost_types_settings
+ render_widget Widget::Settings::Fieldset, @subject, { type: "units" } do
+ render_widget Widget::CostTypes,
+ @cost_types,
+ selected_type_id: @selected_type_id
+ end
+ end
+
+ def render_with_options(options, &block)
+ @cost_types = options.delete(:cost_types)
+ @selected_type_id = options.delete(:selected_type_id)
+
+ super(options, &block)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/widget/simple_table.rb b/vendored-plugins/openproject-reporting/lib/widget/simple_table.rb
new file mode 100644
index 0000000000..df31949e51
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/widget/simple_table.rb
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table::SimpleTable < Widget::Table
+ simple_table self
+
+ def render
+ @list = @subject.map {|r| r.important_fields }.flatten.uniq
+ @show_units = @list.include? "cost_type_id"
+
+ content = content_tag :table, { class: "report", id: "sortable-table" } do
+ concat head
+ concat foot
+ concat body
+ end
+ # FIXME do that js-only, like a man's man
+ render_widget Widget::Table::SortableInit, @subject, to: content
+ write content.html_safe
+ end
+
+ def head
+ content_tag :thead do
+ content_tag :tr do
+ @list.each do |field|
+ concat content_tag(:th, class: "right") { label_for(field) }
+ end
+ concat content_tag(:th, class: "right") { label_for(:units) } if @show_units
+ concat content_tag(:th, class: "right") { label_for(:label_sum) }
+ end
+ end
+ end
+
+ def foot
+ content_tag :tfoot do
+ content_tag :tr do
+ concat content_tag(:th, '', class: "result inner", colspan: @list.size)
+ concat content_tag(:th, show_result(@subject), (@show_units ? {class: "result right", colspan: "2"} : {class: "result right"}))
+ end
+ end
+ end
+
+ def body
+ content_tag :tbody do
+ @subject.each do |result|
+ concat (content_tag :tr, class: cycle("odd", "even") do
+ concat (content_tag :td, :'raw-data' => raw_field(*result.fields.first) do
+ show_row result
+ end)
+ if @show_units
+ concat (content_tag :td, :'raw-data' => result.units do
+ show_result result, result.fields[:cost_type_id].to_i
+ end)
+ end
+ concat (content_tag :td, :'raw-data' => result.real_costs do
+ show_result result
+ end)
+ end)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/lib/widget/sortable_init.rb b/vendored-plugins/openproject-reporting/lib/widget/sortable_init.rb
new file mode 100644
index 0000000000..4fb44feabe
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/lib/widget/sortable_init.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table::SortableInit < Widget::Base
+
+ def render
+ sort_first_row = @options[:sort_first_row] || false
+
+ write (content_tag :script, type: "text/javascript" do
+ content = %Q{// 0) {
+ ts_resortTable(table_date_header.childElements().first(), table_date_header.cellIndex);
+ }
+ }.html_safe
+ end
+ content << "//]]>"
+ end.html_safe)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/openproject-reporting.gemspec b/vendored-plugins/openproject-reporting/openproject-reporting.gemspec
new file mode 100644
index 0000000000..2930d5b685
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/openproject-reporting.gemspec
@@ -0,0 +1,25 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "open_project/reporting/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-reporting"
+ s.version = OpenProject::Reporting::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/plugin-reporting"
+ s.summary = "OpenProject Reporting"
+ s.description = "This plugin allows creating custom cost reports with filtering and grouping created by the OpenProject Costs plugin"
+
+ s.files = Dir["{app,config,db,lib,doc}/**/*", "README.md"]
+ s.test_files = Dir["spec/**/*"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+ s.add_dependency "reporting_engine", ">= 1.1.0"
+ s.add_dependency "openproject-costs", ">= 5.0.1"
+
+ s.add_development_dependency "factory_girl_rails", "~> 4.0"
+end
diff --git a/vendored-plugins/openproject-reporting/spec/controllers/cost_reports_controller_spec.rb b/vendored-plugins/openproject-reporting/spec/controllers/cost_reports_controller_spec.rb
new file mode 100644
index 0000000000..a563f3b237
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/controllers/cost_reports_controller_spec.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe CostReportsController, type: :controller do
+ include OpenProject::Reporting::PluginSpecHelper
+
+ let(:user) { FactoryGirl.build(:user) }
+ let(:project) { FactoryGirl.build(:valid_project) }
+
+ describe "GET show" do
+ before(:each) do
+ is_member project, user, [:view_cost_entries]
+ allow(User).to receive(:current).and_return(user)
+ end
+
+ describe "WHEN providing invalid units
+ WHEN having the view_cost_entries permission" do
+ before do
+ get :show, id: 1, unit: -1
+ end
+
+ it "should respond with a 404 error" do
+ expect(response.code).to eql("404")
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/controllers/custom_fields_controller_spec.rb b/vendored-plugins/openproject-reporting/spec/controllers/custom_fields_controller_spec.rb
new file mode 100644
index 0000000000..0c93181493
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/controllers/custom_fields_controller_spec.rb
@@ -0,0 +1,147 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe CustomFieldsController do
+ let!(:custom_field) { FactoryGirl.create(:work_package_custom_field) }
+ let!(:custom_field_permanent) { FactoryGirl.create(:work_package_custom_field) }
+ let(:custom_field_name) { "CustomField#{custom_field.id}" }
+ let(:custom_field_permanent_name) { "CustomField#{custom_field_permanent.id}" }
+ let(:cost_query) { FactoryGirl.build(:cost_query) }
+
+ before do
+ allow(@controller).to receive(:authorize)
+ allow(@controller).to receive(:check_if_login_required)
+ allow(@controller).to receive(:require_admin)
+
+ CostQuery::Filter::CustomFieldEntries.reset!
+ CostQuery::Filter::CustomFieldEntries.all
+
+ CostQuery::GroupBy::CustomFieldEntries.reset!
+ CostQuery::GroupBy::CustomFieldEntries.all
+ end
+
+ describe '#destroy' do
+ shared_context 'remove custom field' do
+ before do
+ cost_query.filter(custom_field_permanent_name, { operator: '=', values: [] })
+ cost_query.group_by(custom_field_permanent_name)
+ cost_query.save!
+
+ delete :destroy, id: custom_field.id
+ end
+ end
+
+ shared_examples_for 'custom field is removed from cost query' do
+ include_context 'remove custom field'
+
+ shared_examples_for 'custom field removed' do
+ it { expect(subject).not_to include(name) }
+ end
+
+ shared_examples_for 'custom field exists' do
+ it { expect(subject).to include(name) }
+ end
+
+ describe 'custom field is removed from cost query filter' do
+ subject { CostQuery.find(cost_query.id).filters.collect(&:class).collect(&:name) }
+
+ it_behaves_like 'custom field removed' do
+ let(:name) { "CostQuery::Filter::#{custom_field_name}" }
+ end
+ end
+
+ describe 'custom field is removed from cost query group bys' do
+ subject { CostQuery.find(cost_query.id).group_bys.collect(&:class).collect(&:name) }
+
+ it_behaves_like 'custom field removed' do
+ let(:name) { "CostQuery::GroupBy::#{custom_field_name}" }
+ end
+ end
+
+ describe 'permanent custom field still exists in cost query filter' do
+ subject { cost_query.reload.filters.collect(&:class).collect(&:name) }
+
+ it_behaves_like 'custom field exists' do
+ let(:name) { "CostQuery::Filter::#{custom_field_permanent_name}" }
+ end
+ end
+
+ describe 'permanent custom field still exists in cost query group by' do
+ subject { cost_query.reload.group_bys.collect(&:class).collect(&:name) }
+
+ it_behaves_like 'custom field exists' do
+ let(:name) { "CostQuery::GroupBy::#{custom_field_permanent_name}" }
+ end
+ end
+ end
+
+ context 'with custom field filter set' do
+ before do
+ cost_query.filter(custom_field_name, { operator: '=', values: [] })
+ end
+
+ it_behaves_like 'custom field is removed from cost query'
+ end
+
+ context 'with custom field group by set' do
+ before do
+ cost_query.group_by(custom_field_name)
+ end
+
+ it_behaves_like 'custom field is removed from cost query'
+ end
+
+ context 'session' do
+ let(:engine_name) { CostQuery.name.underscore.to_sym }
+ let(:key) { :"custom_field#{custom_field.id}" }
+ let(:query) { { filters:
+ {
+ operators: { user_id: "=", key => "=" },
+ values: { user_id: ["96"], key => "" }
+ },
+ groups: { rows: [key.to_s], columns: [key.to_s] }
+ }
+ }
+ before { session[engine_name] = query }
+
+ describe 'does not contain custom field reference' do
+ include_context 'remove custom field'
+
+ it { expect(session[engine_name][:filters][:operators][key]).to be_nil }
+
+ it { expect(session[engine_name][:filters][:values][key]).to be_nil }
+
+ it { expect(session[engine_name][:groups][:rows]).not_to include(key.to_s) }
+
+ it { expect(session[engine_name][:groups][:columns]).not_to include(key.to_s) }
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/disabled_specs.rb b/vendored-plugins/openproject-reporting/spec/disabled_specs.rb
new file mode 100644
index 0000000000..ae1a5a1eea
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/disabled_specs.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'rspec/example_disabler'
+
+RSpec::ExampleDisabler.disable_example('Top menu items as a user with permissions displays all options', 'plugin openproject-reporting removes the menu item')
+RSpec::ExampleDisabler.disable_example('Top menu items as an admin visits the time sheet page', 'plugin openproject-reporting removes the menu item')
+RSpec::ExampleDisabler.disable_example('Top menu items as an admin displays all items', 'plugin openproject-reporting removes the menu item')
diff --git a/vendored-plugins/openproject-reporting/spec/factories/cost_query_factory.rb b/vendored-plugins/openproject-reporting/spec/factories/cost_query_factory.rb
new file mode 100644
index 0000000000..1e17931610
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/factories/cost_query_factory.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+FactoryGirl.define do
+ factory :cost_query do
+ association :user, factory: :user
+ association :project, factory: :project
+ sequence(:name) { |n| "Cost Query #{n}" }
+ factory :private_cost_query do
+ is_public false
+ end
+ factory :public_cost_query do
+ is_public true
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/features/menu_spec.rb b/vendored-plugins/openproject-reporting/spec/features/menu_spec.rb
new file mode 100644
index 0000000000..e79fb0d44b
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/features/menu_spec.rb
@@ -0,0 +1,123 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe 'project menu', type: :feature do
+ let(:current_user) { FactoryGirl.create :admin }
+ let!(:project) { FactoryGirl.create :valid_project, identifier: 'ponyo', name: 'Ponyo' }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ # remove filters that might be left overs from former specs
+ CostQuery::Cache.reset!
+ end
+
+ ##
+ # Depending on the current page the link to the cost reports was broken.
+ # This seems to be due to a peculiarity of the rails routing where
+ # `url_for controller: :foo` would return a link relative to the controller
+ # handling the current request path if the controller was routed to via a
+ # namespaced route.
+ #
+ # Example:
+ #
+ # `url_for controller: 'cost_reports'` will yield different results ...
+ #
+ # when on `/projects/ponyo/work_packages`: `/projects/ponyo/cost_reports` (correct)
+ # when on `/projects/ponyo/work_packages/calendar`: `/work_packages/cost_reports?project_id=ponyo`
+ #
+ # This is only relevant for project menu entries, not global ones (`project_id` param is nil)*.
+ # Meaning that you have to make sure to force the absolute URL in a project menu entry
+ # by specificying the controller as e.g. '/cost_reports' instead of just 'cost_reports'.
+ #
+ # Refer to `engine.rb` to see where the menu entries are declared.
+ #
+ # * May apply to routes used with parameters in general.
+ describe '#18788 (cost reports not found (404)) regression test' do
+ describe 'link to project cost reports' do
+ shared_examples 'it leads to the project costs reports' do
+ before do
+ visit current_path
+ end
+
+ it 'leads to cost reports' do
+ click_on 'Cost reports'
+
+ expect(page).to have_selector('.breadcrumb > li', text: 'Ponyo')
+ expect(page).to have_selector('.breadcrumb > li', text: 'Cost reports')
+ end
+ end
+
+ context "when on the project's activity page" do
+ let(:current_path) { '/projects/ponyo/activity' }
+
+ it_behaves_like 'it leads to the project costs reports'
+ end
+
+ context "when on the project's calendar" do
+ let(:current_path) { '/projects/ponyo/work_packages/calendar' }
+
+ it_behaves_like 'it leads to the project costs reports'
+ end
+ end
+
+ describe 'link to global cost reports' do
+ shared_examples 'it leads to the cost reports' do
+ before do
+ visit current_path
+ end
+
+ it 'leads to cost reports' do
+ # doing what no human can - click on invisible items.
+ # This way, we avoid having to use selenium and by that increase stability.
+ within '#more-menu ul', visible: false do
+ click_on 'Cost reports', visible: false
+ end
+
+ expect(page).to have_selector('.breadcrumb > li', text: 'Cost reports')
+
+ # to make sure we're not seeing the project cost reports:
+ expect(page).not_to have_text('Ponyo')
+ end
+ end
+
+ context "when on the project's activity page" do
+ let(:current_path) { '/projects/ponyo/activity' }
+
+ it_behaves_like 'it leads to the cost reports'
+ end
+
+ context "when on the project's calendar" do
+ let(:current_path) { '/projects/ponyo/work_packages/calendar' }
+
+ it_behaves_like 'it leads to the cost reports'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/lib/open_project/configuration_spec.rb b/vendored-plugins/openproject-reporting/spec/lib/open_project/configuration_spec.rb
new file mode 100644
index 0000000000..228cc82973
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/lib/open_project/configuration_spec.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+
+describe 'OpenProject::Configuration' do
+ context '.cost_reporting_cache_filter_classes' do
+ before do
+ # This prevents the values from the actual configuration file to influence
+ # the test outcome.
+ #
+ # TODO: I propose to port this over to the core to always prevent this for specs.
+ OpenProject::Configuration.load(file: 'bogus')
+ end
+
+ after do
+ # resetting for now to avoid braking specs, who by now rely on having the file read.
+ OpenProject::Configuration.load
+ end
+
+ it 'is a true by default via the method' do
+ expect(OpenProject::Configuration.cost_reporting_cache_filter_classes).to be_truthy
+ end
+
+
+ it 'is true by default via the hash' do
+ expect(OpenProject::Configuration['cost_reporting_cache_filter_classes']).to be_truthy
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/lib/open_project/reporting/default_data_spec.rb b/vendored-plugins/openproject-reporting/spec/lib/open_project/reporting/default_data_spec.rb
new file mode 100644
index 0000000000..6e46e04924
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/lib/open_project/reporting/default_data_spec.rb
@@ -0,0 +1,58 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.rdoc for more details.
+#++
+
+require 'spec_helper'
+
+describe OpenProject::Reporting::DefaultData do
+ let(:seeder) { BasicData::RoleSeeder.new }
+ let(:project_admin) { OpenProject::Reporting::DefaultData.project_admin_role }
+ let(:permissions) do
+ OpenProject::Reporting::DefaultData.restricted_project_admin_permissions
+ end
+
+ before do
+ allow(seeder).to receive(:builtin_roles).and_return([])
+
+ seeder.seed!
+ end
+
+ it 'removes permissions from the project admin role' do
+ expect(project_admin.permissions).not_to include *permissions
+ end
+
+ it 'is not loaded again on existing data' do
+ project_admin.permissions.concat permissions
+ project_admin.save!
+
+ # on existing data the permissions should not be removed
+ seeder.seed!
+
+ project_admin.reload
+ expect(project_admin.permissions).to include *permissions
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/cache_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/cache_spec.rb
new file mode 100644
index 0000000000..5594fe1183
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/cache_spec.rb
@@ -0,0 +1,124 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+require File.join(File.dirname(__FILE__), '..', '..', 'support', 'configuration_helper')
+
+describe CostQuery::Cache do
+ include OpenProject::Reporting::SpecHelper::ConfigurationHelper
+
+ def all_caches
+ [ CostQuery::GroupBy::CustomFieldEntries,
+ CostQuery::GroupBy,
+ CostQuery::Filter::CustomFieldEntries,
+ CostQuery::Filter ]
+ end
+
+ def expect_reset_on_caches
+ all_caches.each do |klass|
+ expect(klass).to receive(:reset!)
+ end
+ end
+
+ def expect_no_reset_on_caches
+ all_caches.each do |klass|
+ expect(klass).to_not receive(:reset!)
+ end
+ end
+
+ def reset_cache_keys
+ # resetting internal caching keys to avoid dependencies with other specs
+ described_class.send(:latest_custom_field_change=, nil)
+ described_class.send(:custom_field_count=, 0)
+ end
+
+ def custom_fields_exist
+ allow(WorkPackageCustomField).to receive(:maximum).and_return(Time.now)
+ allow(WorkPackageCustomField).to receive(:count).and_return(23)
+ end
+
+ def no_custom_fields_exist
+ allow(WorkPackageCustomField).to receive(:maximum).and_return(nil)
+ allow(WorkPackageCustomField).to receive(:count).and_return(0)
+ end
+
+ before do
+ reset_cache_keys
+ end
+
+ after do
+ reset_cache_keys
+ end
+
+ describe '.check' do
+
+ context 'with cache_classes configuration enabled' do
+ before do
+ mock_cache_classes_setting_with(true)
+ end
+
+ it 'resets the caches on filters and group by' do
+ custom_fields_exist
+ expect_reset_on_caches
+
+ described_class.check
+ end
+
+ it 'stores when the last update was made and does not reset again if nothing changed' do
+ custom_fields_exist
+ expect_reset_on_caches
+
+ described_class.check
+
+ expect_no_reset_on_caches
+
+ described_class.check
+ end
+
+ it 'does reset the cache if last CustomField is removed' do
+ custom_fields_exist
+ expect_reset_on_caches
+
+ described_class.check
+
+ no_custom_fields_exist
+ expect_reset_on_caches
+
+ described_class.check
+ end
+ end
+
+ context 'with_cache_classes configuration disabled' do
+ before do
+ mock_cache_classes_setting_with(false)
+ end
+
+ it 'resets the cache again even if nothing changed' do
+ custom_fields_exist
+ expect_reset_on_caches
+
+ described_class.check
+
+ expect_reset_on_caches
+
+ described_class.check
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/chaining_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/chaining_spec.rb
new file mode 100644
index 0000000000..6743096244
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/chaining_spec.rb
@@ -0,0 +1,320 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ let(:project) { FactoryGirl.create(:project) }
+
+ minimal_query
+
+ describe '#chain' do
+ before do
+ #FIXME: is there a better way to load all filter and groups?
+ CostQuery::Filter.all && CostQuery::GroupBy.all
+ CostQuery.chain_initializer.clear
+ end
+
+ after(:all) do
+ CostQuery.chain_initializer.clear
+ end
+
+ it "should contain NoFilter" do
+ expect(@query.chain).to be_a(CostQuery::Filter::NoFilter)
+ end
+
+ it "should keep NoFilter at bottom" do
+ @query.filter :project_id
+ expect(@query.chain.bottom).to be_a(CostQuery::Filter::NoFilter)
+ expect(@query.chain.top).not_to be_a(CostQuery::Filter::NoFilter)
+ end
+
+ it "should remember it's correct parent" do
+ @query.group_by :project_id
+ @query.filter :project_id
+ expect(@query.chain.top.child.child.parent).to eq(@query.chain.top.child)
+ end
+
+ it "should place filter after a group_by" do
+ @query.group_by :project_id
+ expect(@query.chain.bottom.parent).to be_a(CostQuery::GroupBy::ProjectId)
+ expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
+
+ @query.filter :project_id
+ expect(@query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
+ expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
+ end
+
+ it "should place rows in front of columns when adding a column first" do
+ @query.column :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:column)
+ expect(@query.chain.top.type).to eq(:column)
+
+ @query.row :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:column)
+ expect(@query.chain.top.type).to eq(:row)
+ end
+
+ it "should place rows in front of columns when adding a row first" do
+ @query.row :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:row)
+ expect(@query.chain.top.type).to eq(:row)
+
+ @query.column :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:column)
+ expect(@query.chain.top.type).to eq(:row)
+ end
+
+ it "should place rows in front of filters" do
+ @query.row :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:row)
+ expect(@query.chain.top.type).to eq(:row)
+
+ @query.filter :project_id
+ expect(@query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
+ expect(@query.chain.top).to be_a(CostQuery::GroupBy::ProjectId)
+ expect(@query.chain.top.type).to eq(:row)
+ end
+
+ it "should place columns in front of filters" do
+ @query.column :project_id
+ expect(@query.chain.bottom.parent.type).to eq(:column)
+ expect(@query.chain.top.type).to eq(:column)
+
+ @query.filter :project_id
+ expect(@query.chain.bottom.parent).to be_a(CostQuery::Filter::ProjectId)
+ expect(@query.chain.top).to be_a(CostQuery::GroupBy::Base)
+ expect(@query.chain.top.type).to eq(:column)
+ end
+
+ it "should return all filters, including the NoFilter" do
+ @query.filter :project_id
+ @query.group_by :project_id
+ expect(@query.filters.size).to eq(2)
+ expect(@query.filters.map {|f| f.class.underscore_name}).to include "project_id"
+ end
+
+ it "should return all group_bys" do
+ @query.filter :project_id
+ @query.group_by :project_id
+ expect(@query.group_bys.size).to eq(1)
+ expect(@query.group_bys.map {|g| g.class.underscore_name}).to include "project_id"
+ end
+
+ it "should initialize the chain through a block" do
+ class TestFilter < Report::Filter::Base
+ def self.engine
+ CostQuery
+ end
+ end
+ TestFilter.send(:initialize_query_with) {|query| query.filter(:project_id, value: project.id)}
+ @query.build_new_chain
+ expect(@query.filters.map {|f| f.class.underscore_name}).to include "project_id"
+ expect(@query.filters.detect {|f| f.class.underscore_name == "project_id"}.values).to eq(Array(project.id))
+ end
+
+ context "store and load" do
+ before do
+ @query.filter :project_id, value: project.id
+ @query.filter :cost_type_id, value: CostQuery::Filter::CostTypeId.available_values.first
+ @query.filter :category_id, value: CostQuery::Filter::CategoryId.available_values.first
+ @query.group_by :activity_id
+ @query.group_by :cost_object_id
+ @query.group_by :cost_type_id
+ @new_query = CostQuery.deserialize(@query.serialize)
+ end
+
+ it "should serialize the chain correctly" do
+ [:filters, :group_bys].each do |type|
+ @query.send(type).each do |chainable|
+ expect(@query.serialize[type].collect{|c| c[0]}).to include chainable.class.name.demodulize
+ end
+ end
+ end
+
+ it "should deserialize a serialized query correctly" do
+ expect(@new_query.serialize).to eq(@query.serialize)
+ end
+
+ it "should keep the order of group bys" do
+ @query.group_bys.each_with_index do |group_by, index|
+ # check for order
+ @new_query.group_bys.each_with_index do |g, ix|
+ if g.class.name == group_by.class.name
+ expect(ix).to eq(index)
+ end
+ end
+ end
+ end
+
+ it "should keep the right filter values" do
+ @query.filters.each_with_index do |filter, index|
+ # check for presence
+ expect(@new_query.filters.any? do |f|
+ f.class.name == filter.class.name && (filter.respond_to?(:values) ? f.values == filter.values : true)
+ end).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe Report::Chainable do
+ describe '#top' do
+ before { @chain = Report::Chainable.new }
+
+ it "returns for an one element long chain that chain as top" do
+ expect(@chain.top).to eq(@chain)
+ expect(@chain).to be_top
+ end
+
+ it "does not keep the old top when prepending elements" do
+ Report::Chainable.new @chain
+ expect(@chain.top).not_to eq(@chain)
+ expect(@chain).not_to be_top
+ end
+
+ it "sets new top when prepending elements" do
+ current = @chain
+ 10.times do
+ old, current = current, CostQuery::Chainable.new(current)
+ expect(old.top).to eq(current)
+ expect(@chain.top).to eq(current)
+ end
+ end
+ end
+
+ describe '#inherited_attribute' do
+ before do
+ @a = Class.new Report::Chainable
+ @a.inherited_attribute :foo, default: 42
+ @b = Class.new @a
+ @c = Class.new @a
+ @d = Class.new @b
+ end
+
+ it 'takes default argument' do
+ expect(@a.foo).to eq(42)
+ expect(@b.foo).to eq(42)
+ expect(@c.foo).to eq(42)
+ expect(@d.foo).to eq(42)
+ end
+
+ it 'inherits values' do
+ @a.foo 1337
+ expect(@d.foo).to eq(1337)
+ end
+
+ it 'does not change values of parents and akin' do
+ @b.foo 1337
+ expect(@a.foo).not_to eq(1337)
+ expect(@c.foo).not_to eq(1337)
+ end
+
+ it 'is able to map values' do
+ @a.inherited_attribute :bar, map: proc { |x| x*2 }
+ @a.bar 21
+ expect(@a.bar).to eq(42)
+ end
+
+ describe '#list' do
+ it "merges lists" do
+ @a.inherited_attribute :bar, list: true
+ @a.bar 1; @b.bar 2; @d.bar 3, 4
+ expect(@a.bar).to eq([1])
+ expect(@b.bar.sort).to eq([1, 2])
+ expect(@c.bar.sort).to eq([1])
+ expect(@d.bar.sort).to eq([1, 2, 3, 4])
+ end
+
+ it "is able to map lists" do
+ @a.inherited_attribute :bar, list: true, map: :to_s
+ @a.bar 1; @b.bar 1; @d.bar 1
+ expect(@a.bar).to eq(%w[1])
+ expect(@b.bar).to eq(%w[1 1])
+ expect(@c.bar).to eq(%w[1])
+ expect(@d.bar).to eq(%w[1 1 1])
+ end
+
+ it "is able to produce uniq lists" do
+ @a.inherited_attribute :bar, list: true, uniq: true
+ @a.bar 1, 1, 2
+ @b.bar 2, 3
+ expect(@b.bar.sort).to eq([1, 2, 3])
+ end
+
+ it "keeps old entries" do
+ @a.inherited_attribute :bar, list: true
+ @a.bar 1
+ @a.bar 2
+ expect(@a.bar.sort).to eq([1, 2])
+ end
+ end
+ end
+
+ describe '#display' do
+ it "should give display? == false when a filter says dont_display!" do
+ class TestFilter < Report::Filter::Base
+ dont_display!
+ end
+ expect(TestFilter.display?).to be false
+ Object.send(:remove_const, :TestFilter)
+ end
+
+ it "should give display? == true when a filter doesn't specify it's visibility" do
+ class TestFilter < Report::Filter::Base
+ end
+ expect(TestFilter.display?).to be true
+ Object.send(:remove_const, :TestFilter)
+ end
+
+ it "should give display? == true when a filter says display!" do
+ class TestFilter < Report::Filter::Base
+ display!
+ end
+ expect(TestFilter.display?).to be true
+ Object.send(:remove_const, :TestFilter)
+ end
+ end
+
+ describe '#selectable' do
+ it "should give selectable? == false when a filter says not_selectable!" do
+ class TestFilter < Report::Filter::Base
+ not_selectable!
+ end
+ expect(TestFilter.selectable?).to be false
+ Object.send(:remove_const, :TestFilter)
+ end
+
+ it "should give selectable? == true when a filter doesn't specify it's selectability" do
+ class TestFilter < Report::Filter::Base
+ end
+ expect(TestFilter.selectable?).to be true
+ Object.send(:remove_const, :TestFilter)
+ end
+
+ it "should give selectable? == true when a filter says selectable!" do
+ class TestFilter < Report::Filter::Base
+ selectable!
+ end
+ expect(TestFilter.selectable?).to be true
+ Object.send(:remove_const, :TestFilter)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/cost_query_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/cost_query_spec.rb
new file mode 100644
index 0000000000..af4aab5e93
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/cost_query_spec.rb
@@ -0,0 +1,106 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe User, "#destroy", type: :model do
+ let(:substitute_user) { DeletedUser.first }
+ let(:private_query) { FactoryGirl.create(:private_cost_query) }
+ let(:public_query) { FactoryGirl.create(:public_cost_query) }
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+
+ describe "WHEN the user has saved private cost queries" do
+
+ before do
+ private_query.user.destroy
+ end
+
+ it { expect(CostQuery.find_by_id(private_query.id)).to eq(nil) }
+ end
+
+ describe "WHEN the user has saved public cost queries" do
+ before do
+ public_query.user.destroy
+ end
+
+ it { expect(CostQuery.find_by_id(public_query.id)).to eq(public_query) }
+ it { expect(public_query.reload.user_id).to eq(substitute_user.id) }
+ end
+
+ shared_examples_for "public query" do
+ let(:filter_symbol) { filter.to_s.demodulize.underscore.to_sym }
+
+ describe "WHEN the filter has the deleted user as it's value" do
+ before do
+ public_query.filter(filter_symbol, values: [user.id.to_s], operator: "=")
+ public_query.save!
+
+ user.destroy
+ end
+
+ it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any?{ |f| f.is_a?(filter) }).to be_falsey }
+ end
+
+ describe "WHEN the filter has another user as it's value" do
+ before do
+ public_query.filter(filter_symbol, values: [user2.id.to_s], operator: "=")
+ public_query.save!
+
+ user.destroy
+ end
+
+ it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any?{ |f| f.is_a?(filter) }).to be_truthy }
+ it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.detect{ |f| f.is_a?(filter) }.values).to eq([user2.id.to_s]) }
+ end
+
+ describe "WHEN the filter has the deleted user and another user as it's value" do
+ before do
+ public_query.filter(filter_symbol, values: [user.id.to_s, user2.id.to_s], operator: "=")
+ public_query.save!
+
+ user.destroy
+ end
+
+ it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any?{ |f| f.is_a?(filter) }).to be_truthy }
+ it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.detect{ |f| f.is_a?(filter) }.values).to eq([user2.id.to_s]) }
+ end
+ end
+
+ describe "WHEN someone has saved a public cost query
+ WHEN the query has a user_id filter" do
+ let(:filter) { CostQuery::Filter::UserId }
+
+ it_should_behave_like "public query"
+ end
+
+ describe "WHEN someone has saved a public cost query
+ WHEN the query has a author_id filter" do
+ let(:filter) { CostQuery::Filter::AuthorId }
+
+ it_should_behave_like "public query"
+ end
+
+ describe "WHEN someone has saved a public cost query
+ WHEN the query has a assigned_to_id filter" do
+ let(:filter) { CostQuery::Filter::AssignedToId }
+
+ it_should_behave_like "public query"
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/filter_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/filter_spec.rb
new file mode 100644
index 0000000000..0ad47d1ef6
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/filter_spec.rb
@@ -0,0 +1,439 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ minimal_query
+
+ let!(:project) { FactoryGirl.create(:project_with_types) }
+ let!(:user) { FactoryGirl.create(:user, member_in_project: project) }
+
+ def create_work_package_with_entry(entry_type, work_package_params={}, entry_params = {})
+ work_package_params = {project: project}.merge!(work_package_params)
+ work_package = FactoryGirl.create(:work_package, work_package_params)
+ entry_params = {work_package: work_package,
+ project: work_package_params[:project],
+ user: user}.merge!(entry_params)
+ FactoryGirl.create(entry_type, entry_params)
+ work_package
+ end
+
+ describe CostQuery::Filter do
+ def create_work_package_with_time_entry(work_package_params={}, entry_params = {})
+ create_work_package_with_entry(:time_entry, work_package_params, entry_params)
+ end
+
+ it "shows all entries when no filter is applied" do
+ expect(@query.result.count).to eq(Entry.count)
+ end
+
+ it "always sets cost_type" do
+ @query.result.each do |result|
+ expect(result["cost_type"]).not_to be_nil
+ end
+ end
+
+ it "sets activity_id to -1 for cost entries" do
+ @query.result.each do |result|
+ expect(result["activity_id"].to_i).to eq(-1) if result["type"] != "TimeEntry"
+ end
+ end
+
+ # Test Work Package attributes that are included in of the result set
+
+ [
+ [CostQuery::Filter::ProjectId, 'project', "project_id", 2],
+ [CostQuery::Filter::UserId, 'user', "user_id", 2],
+ [CostQuery::Filter::CostTypeId, 'cost_type', "cost_type_id", 1],
+ [CostQuery::Filter::WorkPackageId, 'work_package', "work_package_id", 2],
+ [CostQuery::Filter::ActivityId, 'activity', "activity_id", 1],
+ ].each do |filter, object_name, field, expected_count|
+ describe filter do
+ let!(:non_matching_entry) { FactoryGirl.create(:cost_entry) }
+ let!(:object) { send(object_name) }
+ let!(:author) { FactoryGirl.create(:user, member_in_project: project) }
+ let!(:work_package) { FactoryGirl.create(:work_package, project: project,
+ author: author) }
+ let!(:cost_type) { FactoryGirl.create(:cost_type) }
+ let!(:cost_entry) { FactoryGirl.create(:cost_entry, work_package: work_package,
+ user: user,
+ project: project,
+ cost_type: cost_type) }
+ let!(:activity) { FactoryGirl.create(:time_entry_activity) }
+ let!(:time_entry) { FactoryGirl.create(:time_entry, work_package: work_package,
+ user: user,
+ project: project,
+ activity: activity) }
+
+ it "should only return entries from the given #{filter.to_s}" do
+ @query.filter field, value: object.id
+ @query.result.each do |result|
+ expect(result[field].to_s).to eq(object.id.to_s)
+ end
+ end
+
+ it "should allow chaining the same filter" do
+ @query.filter field, value: object.id
+ @query.filter field, value: object.id
+ @query.result.each do |result|
+ expect(result[field].to_s).to eq(object.id.to_s)
+ end
+ end
+
+ it "should return no results for excluding filters" do
+ @query.filter field, value: object.id
+ @query.filter field, value: object.id + 1
+ expect(@query.result.count).to eq(0)
+ end
+
+ it "should compute the correct number of results" do
+ @query.filter field, value: object.id
+ expect(@query.result.count).to eq(expected_count)
+ end
+ end
+ end
+
+ # Test author attribute separately as it is not included in the result set
+
+ describe CostQuery::Filter::AuthorId do
+ let!(:non_matching_entry) { FactoryGirl.create(:cost_entry) }
+ let!(:author) { FactoryGirl.create(:user, member_in_project: project) }
+ let!(:work_package) { FactoryGirl.create(:work_package, project: project,
+ author: author) }
+ let!(:cost_type) { FactoryGirl.create(:cost_type) }
+ let!(:cost_entry) { FactoryGirl.create(:cost_entry, work_package: work_package,
+ user: user,
+ project: project,
+ cost_type: cost_type) }
+ let!(:activity) { FactoryGirl.create(:time_entry_activity) }
+ let!(:time_entry) { FactoryGirl.create(:time_entry, work_package: work_package,
+ user: user,
+ project: project,
+ activity: activity) }
+
+ it "should only return entries from the given CostQuery::Filter::AuthorId" do
+ @query.filter 'author_id', value: author.id
+ @query.result.each do |result|
+ work_package_id = result["work_package_id"]
+ expect(WorkPackage.find(work_package_id).author.id).to eq(author.id)
+ end
+ end
+
+ it "should allow chaining the same filter" do
+ @query.filter 'author_id', value: author.id
+ @query.filter 'author_id', value: author.id
+ @query.result.each do |result|
+ work_package_id = result["work_package_id"]
+ expect(WorkPackage.find(work_package_id).author.id).to eq(author.id)
+ end
+ end
+
+ it "should return no results for excluding filters" do
+ @query.filter 'author_id', value: author.id
+ @query.filter 'author_id', value: author.id + 1
+ expect(@query.result.count).to eq(0)
+ end
+
+ it "should compute the correct number of results" do
+ @query.filter 'author_id', value: author.id
+ expect(@query.result.count).to eq(2)
+ end
+ end
+
+ it "filters spent_on" do
+ @query.filter :spent_on, operator: 'w'
+ expect(@query.result.count).to eq(Entry.all.select { |e| e.spent_on.cweek == TimeEntry.all.first.spent_on.cweek }.count)
+ end
+
+ it "filters created_on" do
+ @query.filter :created_on, operator: 't'
+ # we assume that some of our fixtures set created_on to Time.now
+ expect(@query.result.count).to eq(Entry.all.select { |e| e.created_on.to_date == Date.today }.count)
+ end
+
+ it "filters updated_on" do
+ @query.filter :updated_on, value: Date.today.years_ago(20), operator: '>d'
+ # we assume that our were updated in the last 20 years
+ expect(@query.result.count).to eq(Entry.all.select { |e| e.updated_on.to_date > Date.today.years_ago(20) }.count)
+ end
+
+ it "filters user_id" do
+ old_user = User.current
+ # create non-matching entry
+ anonymous = FactoryGirl.create(:anonymous)
+ create_work_package_with_time_entry({}, {user: anonymous})
+ # create matching entry
+ create_work_package_with_time_entry()
+ @query.filter :user_id, value: user.id, operator: '='
+ expect(@query.result.count).to eq(1)
+ end
+
+ describe "work_package-based filters" do
+ def create_work_packages_and_time_entries(entry_count, work_package_params={}, entry_params={})
+ entry_count.times do
+ create_work_package_with_entry(:cost_entry, work_package_params, entry_params)
+ end
+ end
+
+ def create_matching_object_with_time_entries(factory, work_package_field, entry_count)
+ object = FactoryGirl.create(factory)
+ create_work_packages_and_time_entries(entry_count, {work_package_field => object})
+ object
+ end
+
+ it "filters overridden_costs" do
+ @query.filter :overridden_costs, operator: 'y'
+ expect(@query.result.count).to eq(Entry.all.select { |e| not e.overridden_costs.nil? }.count)
+ end
+
+ it "filters status" do
+ matching_status = FactoryGirl.create(:status, is_closed: true)
+ create_work_packages_and_time_entries(3, status: matching_status)
+ @query.filter :status_id, operator: 'c'
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters types" do
+ matching_type = project.types.first
+ create_work_packages_and_time_entries(3, type: matching_type)
+ @query.filter :type_id, operator: '=', value: matching_type.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters work_package authors" do
+ matching_author = create_matching_object_with_time_entries(:user, :author, 3)
+ @query.filter :author_id, operator: '=', value: matching_author.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters priority" do
+ matching_priority = create_matching_object_with_time_entries(:priority, :priority, 3)
+ @query.filter :priority_id, operator: '=', value: matching_priority.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters assigned to" do
+ matching_user = create_matching_object_with_time_entries(:user, :assigned_to, 3)
+ @query.filter :assigned_to_id, operator: '=', value: matching_user.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters category" do
+ category = FactoryGirl.create(:category, project: project)
+ create_work_packages_and_time_entries(3, category: category)
+ @query.filter :category_id, operator: '=', value: category.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters target version" do
+ matching_version = FactoryGirl.create(:version, project: project)
+ create_work_packages_and_time_entries(3, fixed_version: matching_version)
+
+ @query.filter :fixed_version_id, operator: '=', value: matching_version.id
+ expect(@query.result.count).to eq(3)
+ end
+
+ it "filters subject" do
+ matching_work_package = create_work_package_with_time_entry(subject: 'matching subject')
+ @query.filter :subject, operator: '=', value: 'matching subject'
+ expect(@query.result.count).to eq(1)
+ end
+
+ it "filters start" do
+ start_date = Date.new(2013, 1, 1)
+ matching_work_package = create_work_package_with_time_entry(start_date: start_date)
+ @query.filter :start_date, operator: '=d', value: start_date
+ expect(@query.result.count).to eq(1)
+ #Entry.all.select { |e| e.work_package.start_date == WorkPackage.all(:order => "id ASC").first.start_date }.count
+ end
+
+ it "filters due date" do
+ due_date = Date.new(2013, 1, 1)
+ matching_work_package = create_work_package_with_time_entry(due_date: due_date)
+ @query.filter :due_date, operator: '=d', value: due_date
+ expect(@query.result.count).to eq(1)
+ #Entry.all.select { |e| e.work_package.due_date == WorkPackage.all(:order => "id ASC").first.due_date }.count
+ end
+
+ it "raises an error if operator is not supported" do
+ expect { @query.filter :spent_on, operator: 'c' }.to raise_error(ArgumentError)
+ end
+ end
+
+ #filter for specific objects, which can't be null
+ [
+ CostQuery::Filter::UserId,
+ CostQuery::Filter::CostTypeId,
+ CostQuery::Filter::WorkPackageId,
+ CostQuery::Filter::AuthorId,
+ CostQuery::Filter::ActivityId,
+ CostQuery::Filter::PriorityId,
+ CostQuery::Filter::TypeId
+ ].each do |filter|
+ it "should only allow default operators for #{filter}" do
+ expect(filter.new.available_operators.uniq.sort).to eq(CostQuery::Operator.default_operators.uniq.sort)
+ end
+ end
+
+ #filter for specific objects, which might be null
+ [
+ CostQuery::Filter::AssignedToId,
+ CostQuery::Filter::CategoryId,
+ CostQuery::Filter::FixedVersionId
+ ].each do |filter|
+ it "should only allow default+null operators for #{filter}" do
+ expect(filter.new.available_operators.uniq.sort).to eq((CostQuery::Operator.default_operators + CostQuery::Operator.null_operators).sort)
+ end
+ end
+
+ #filter for time/date
+ [
+ CostQuery::Filter::CreatedOn,
+ CostQuery::Filter::UpdatedOn,
+ CostQuery::Filter::SpentOn,
+ CostQuery::Filter::StartDate,
+ CostQuery::Filter::DueDate
+ ].each do |filter|
+ it "should only allow time operators for #{filter}" do
+ expect(filter.new.available_operators.uniq.sort).to eq(CostQuery::Operator.time_operators.sort)
+ end
+ end
+
+ describe CostQuery::Filter::CustomFieldEntries do
+ let!(:custom_field) do
+ cf = FactoryGirl.create(:work_package_custom_field,
+ name: 'My custom field')
+ clear_cache
+ cf
+ end
+
+ let(:custom_field2) do
+ FactoryGirl.build(:work_package_custom_field, name: 'Database',
+ field_format: "list",
+ possible_values: ['value'])
+ end
+
+ after(:all) do
+ clear_cache
+ end
+
+ def clear_cache
+ CostQuery::Cache.reset!
+ CostQuery::Filter::CustomFieldEntries.all
+ end
+
+ def delete_work_package_custom_field(cf)
+ cf.destroy
+ clear_cache
+ end
+
+ def update_work_package_custom_field(name, options)
+ fld = WorkPackageCustomField.find_by(name: name)
+ options.each_pair {|k, v| fld.send(:"#{k}=", v) }
+ fld.save!
+ clear_cache
+ end
+
+ include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper
+
+ it "should create classes for custom fields that get added after starting the server" do
+ custom_field
+ expect { filter_class_name_string(custom_field).constantize }.not_to raise_error
+ end
+
+ it "should remove the custom field classes after it is deleted" do
+ custom_field
+ class_name = filter_class_name_string(custom_field)
+ delete_work_package_custom_field(custom_field)
+ expect { filter_class_name_string(custom_field).constantize }.to raise_error NameError
+ end
+
+ it "should provide the correct available values" do
+ custom_field2.save
+
+ clear_cache
+ ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name)
+ CostQuery::Operator.null_operators.each do |o|
+ expect(ao).to include o.name
+ end
+ end
+
+ it "should update the available values on change" do
+ custom_field2.save
+
+ update_work_package_custom_field("Database", field_format: "string")
+ ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name)
+ CostQuery::Operator.string_operators.each do |o|
+ expect(ao).to include o.name
+ end
+ update_work_package_custom_field("Database", field_format: "int")
+ ao = filter_class_name_string(custom_field2).constantize.available_operators.map(&:name)
+ CostQuery::Operator.integer_operators.each do |o|
+ expect(ao).to include o.name
+ end
+ end
+
+ it "includes custom fields classes in CustomFieldEntries.all" do
+ custom_field
+ expect(CostQuery::Filter::CustomFieldEntries.all).
+ to include(filter_class_name_string(custom_field).constantize)
+ end
+
+ it "includes custom fields classes in Filter.all" do
+ custom_field
+ expect(CostQuery::Filter.all).
+ to include(filter_class_name_string(custom_field).constantize)
+ end
+
+ def create_searchable_fields_and_values
+ searchable_field = FactoryGirl.create(:work_package_custom_field,
+ field_format: "text",
+ name: "Searchable Field")
+ 2.times do
+ work_package = create_work_package_with_entry(:cost_entry)
+ FactoryGirl.create(:work_package_custom_value,
+ custom_field: searchable_field,
+ customized: work_package,
+ value: "125")
+ end
+ work_package = create_work_package_with_entry(:cost_entry)
+ FactoryGirl.create(:custom_value,
+ custom_field: searchable_field,
+ value: "non-matching value")
+ clear_cache
+ end
+
+ it "is usable as filter" do
+ create_searchable_fields_and_values
+ id = WorkPackageCustomField.find_by(name: "Searchable Field").id
+ @query.filter "custom_field_#{id}".to_sym, operator: '=', value: "125"
+ expect(@query.result.count).to eq(2)
+ end
+
+ it "is usable as filter #2" do
+ create_searchable_fields_and_values
+ id = WorkPackageCustomField.find_by(name: "Searchable Field").id
+ @query.filter "custom_field_#{id}".to_sym, operator: '=', value: "finnlabs"
+ expect(@query.result.count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/group_by_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/group_by_spec.rb
new file mode 100644
index 0000000000..c090f91554
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/group_by_spec.rb
@@ -0,0 +1,294 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+require File.join(File.dirname(__FILE__), '..', '..', 'support', 'custom_field_filter')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ let!(:type) { FactoryGirl.create(:type) }
+ let!(:project1){ FactoryGirl.create(:project_with_types, types: [type]) }
+ let!(:work_package1) { FactoryGirl.create(:work_package, project: project1, type: type)}
+ let!(:time_entry1) { FactoryGirl.create(:time_entry, work_package: work_package1, project: project1, spent_on: Date.new(2012, 1, 1)) }
+ let!(:time_entry2) do
+ time_entry2 = time_entry1.dup
+ time_entry2.save!
+ time_entry2
+ end
+ let!(:cost_object1) { FactoryGirl.create(:cost_object, project: project1) }
+ let!(:cost_entry1) { FactoryGirl.create(:cost_entry, work_package: work_package1, project: project1, spent_on: Date.new(2013, 2, 3)) }
+ let!(:cost_entry2) do
+ cost_entry2 = cost_entry1.dup
+ cost_entry2.save!
+ cost_entry2
+ end
+
+ let!(:project2) { FactoryGirl.create(:project_with_types, types: [type]) }
+ let!(:work_package2) { FactoryGirl.create(:work_package, project: project2, type: type) }
+ let!(:time_entry3) { FactoryGirl.create(:time_entry, work_package: work_package2, project: project2, spent_on: Date.new(2013, 2, 3)) }
+ let!(:time_entry4) do
+ time_entry4 = time_entry3.dup
+ time_entry4.save!
+ time_entry4
+ end
+ let!(:cost_object2) { FactoryGirl.create(:cost_object, project: project2) }
+ let!(:cost_entry3) { FactoryGirl.create(:cost_entry, work_package: work_package2, project: project2, spent_on: Date.new(2012, 1, 1)) }
+ let!(:cost_entry4) do
+ cost_entry4 = cost_entry3.dup
+ cost_entry4.save!
+ cost_entry4
+ end
+
+ minimal_query
+
+ describe CostQuery::GroupBy do
+ it "should compute group_by on projects" do
+ @query.group_by :project_id
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should keep own and all parents' group fields in all_group_fields" do
+ @query.group_by :project_id
+ @query.group_by :work_package_id
+ @query.group_by :cost_type_id
+ expect(@query.all_group_fields).to eq(%w[entries.cost_type_id])
+ expect(@query.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id])
+ expect(@query.child.child.all_group_fields).to eq(%w[entries.cost_type_id entries.work_package_id entries.project_id])
+ end
+
+ it "should compute group_by WorkPackage" do
+ @query.group_by :work_package_id
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should compute group_by CostType" do
+ @query.group_by :cost_type_id
+ # type 'Labor' for time entries, 2 different cost types
+ expect(@query.result.size).to eq(3)
+ end
+
+ it "should compute group_by Activity" do
+ @query.group_by :activity_id
+ # "-1" for time entries, 2 different cost activities
+ expect(@query.result.size).to eq(3)
+ end
+
+ it "should compute group_by Date (day)" do
+ @query.group_by :spent_on
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should compute group_by Date (week)" do
+ @query.group_by :tweek
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should compute group_by Date (month)" do
+ @query.group_by :tmonth
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should compute group_by Date (year)" do
+ @query.group_by :tyear
+ expect(@query.result.size).to eq(2)
+ end
+
+ it "should compute group_by User" do
+ @query.group_by :user_id
+ expect(@query.result.size).to eq(4)
+ end
+
+ it "should compute group_by Type" do
+ @query.group_by :type_id
+ expect(@query.result.size).to eq(1)
+ end
+
+ it "should compute group_by CostObject" do
+ @query.group_by :cost_object_id
+ expect(@query.result.size).to eq(1)
+ end
+
+ it "should compute multiple group_by" do
+ @query.group_by :project_id
+ @query.group_by :user_id
+ sql_result = @query.result
+
+ expect(sql_result.size).to eq(4)
+ # for each user the number of projects should be correct
+ sql_sizes = []
+ sql_result.each do |sub_result|
+ # user should be the outmost group_by
+ expect(sub_result.fields).to include(:user_id)
+ sql_sizes.push sub_result.size
+ sub_result.each { |sub_sub_result| expect(sub_sub_result.fields).to include(:project_id) }
+ end
+ expect(sql_sizes.sort).to eq([1, 1, 1, 1])
+ end
+
+ # TODO: ?
+ it "should compute multiple group_by with joins" do
+ @query.group_by :project_id
+ @query.group_by :type_id
+ sql_result = @query.result
+ expect(sql_result.size).to eq(1)
+ # for each type the number of projects should be correct
+ sql_sizes = []
+ sql_result.each do |sub_result|
+ # type should be the outmost group_by
+ expect(sub_result.fields).to include(:type_id)
+ sql_sizes.push sub_result.size
+ sub_result.each { |sub_sub_result| expect(sub_sub_result.fields).to include(:project_id) }
+ end
+ expect(sql_sizes.sort).to eq([2])
+ end
+
+ it "compute count correct with lots of group_by" do
+ @query.group_by :project_id
+ @query.group_by :work_package_id
+ @query.group_by :cost_type_id
+ @query.group_by :activity_id
+ @query.group_by :spent_on
+ @query.group_by :tweek
+ @query.group_by :type_id
+ @query.group_by :tmonth
+ @query.group_by :tyear
+
+ expect(@query.result.count).to eq(8)
+ end
+
+ it "should accept row as a specialised group_by" do
+ @query.row :project_id
+ expect(@query.chain.type).to eq(:row)
+ end
+
+ it "should accept column as a specialised group_by" do
+ @query.column :project_id
+ expect(@query.chain.type).to eq(:column)
+ end
+
+ it "should have type :column as a default" do
+ @query.group_by :project_id
+ expect(@query.chain.type).to eq(:column)
+ end
+
+ it "should aggregate a third group_by which owns at least 2 sub results" do
+
+ @query.group_by :tweek
+ @query.group_by :project_id
+ @query.group_by :user_id
+ sql_result = @query.result
+
+ expect(sql_result.size).to eq(4)
+ # for each user the number of projects should be correct
+ sql_sizes = []
+ sub_sql_sizes = []
+ sql_result.each do |sub_result|
+ # user should be the outmost group_by
+ expect(sub_result.fields).to include(:user_id)
+ sql_sizes.push sub_result.size
+
+ sub_result.each do |sub_sub_result|
+ expect(sub_sub_result.fields).to include(:project_id)
+ sub_sql_sizes.push sub_sub_result.size
+
+ sub_sub_result.each do |sub_sub_sub_result|
+ expect(sub_sub_sub_result.fields).to include(:tweek)
+ end
+ end
+ end
+ expect(sql_sizes.sort).to eq([1, 1, 1, 1])
+ expect(sub_sql_sizes.sort).to eq([1, 1, 1, 1])
+ end
+
+ describe CostQuery::GroupBy::CustomFieldEntries do
+ let!(:project){ FactoryGirl.create(:project_with_types) }
+ let!(:custom_field) do
+ FactoryGirl.create(:work_package_custom_field)
+ end
+
+ let(:custom_field2) do
+ FactoryGirl.build(:work_package_custom_field)
+ end
+
+ before do
+ check_cache
+ CostQuery::GroupBy.all.merge CostQuery::GroupBy::CustomFieldEntries.all
+ end
+
+ def check_cache
+ CostQuery::Cache.reset!
+ CostQuery::GroupBy::CustomFieldEntries.all
+ end
+
+ def delete_work_package_custom_field(custom_field)
+ custom_field.destroy
+ check_cache
+ end
+
+ include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper
+
+ it "should create classes for custom fields" do
+ # Would raise a name error
+ expect { group_by_class_name_string(custom_field).constantize }.to_not raise_error
+ end
+
+ it "should create new classes for custom fields that get added after starting the server" do
+ custom_field2.save!
+
+ check_cache
+
+ # Would raise a name error
+ expect { group_by_class_name_string(custom_field2).constantize }.to_not raise_error
+
+ custom_field2.destroy
+ end
+
+ it "should remove the custom field classes after it is deleted" do
+ custom_field2.save!
+
+ check_cache
+
+ custom_field2.destroy
+
+ check_cache
+
+ expect { group_by_class_name_string(custom_field2).constantize }.to raise_error NameError
+ end
+
+ it "includes custom fields classes in CustomFieldEntries.all" do
+ expect(CostQuery::GroupBy::CustomFieldEntries.all).
+ to include(group_by_class_name_string(custom_field).constantize)
+ end
+
+ it "includes custom fields classes in GroupBy.all" do
+ expect(CostQuery::GroupBy.all).
+ to include(group_by_class_name_string(custom_field).constantize)
+ end
+
+ it "is usable as filter" do
+ custom_field2.save!
+
+ check_cache
+
+ @query.group_by "custom_field_#{custom_field2.id}".to_sym
+ footprint = @query.result.each_direct_result.map { |c| [c.count, c.units.to_i] }.sort
+ expect(footprint).to eq([[8, 8]])
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/integration_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/integration_spec.rb
new file mode 100644
index 0000000000..adf19f56ea
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/integration_spec.rb
@@ -0,0 +1,105 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ minimal_query
+
+ let!(:project1){ FactoryGirl.create(:project_with_types) }
+ let!(:work_package1) { FactoryGirl.create(:work_package, project: project1) }
+ let!(:time_entry1) { FactoryGirl.create(:time_entry, work_package: work_package1, project: project1) }
+ let!(:time_entry2) { FactoryGirl.create(:time_entry, work_package: work_package1, project: project1) }
+
+ let!(:project2) { FactoryGirl.create(:project_with_types) }
+ let!(:work_package2) { FactoryGirl.create(:work_package, project: project2) }
+ let!(:time_entry3) { FactoryGirl.create(:time_entry, work_package: work_package2, project: project2) }
+ let!(:time_entry4) { FactoryGirl.create(:time_entry, work_package: work_package2, project: project2) }
+
+ before do
+ FactoryGirl.create(:admin)
+ end
+
+ describe "the reporting system" do
+ it "should compute group_by and a filter" do
+ @query.group_by :project_id
+ @query.filter :status_id, operator: 'o'
+ sql_result = @query.result
+
+ expect(sql_result.size).to eq(2)
+ #for each project the number of entries should be correct
+ sql_count = []
+ sql_result.each do |sub_result|
+ #project should be the outmost group_by
+ expect(sub_result.fields).to include(:project_id)
+ sql_count.push sub_result.count
+ end
+ expect(sql_count.sort).to eq([2, 2])
+ end
+
+ it "should apply two filter and a group_by correctly" do
+ @query.filter :project_id, operator: '=', value: [project1.id]
+ @query.group_by :user_id
+ @query.filter :overridden_costs, operator: 'n'
+
+ sql_result = @query.result
+ expect(sql_result.size).to eq(2)
+ #for each user the number of entries should be correct
+ sql_count = []
+ sql_result.each do |sub_result|
+ #project should be the outmost group_by
+ expect(sub_result.fields).to include(:user_id)
+ sql_count.push sub_result.count
+ end
+ expect(sql_count.sort).to eq([1, 1])
+ end
+
+ it "should apply two different filters on the same field" do
+ @query.filter :project_id, operator: '=', value: [project1.id, project2.id]
+ @query.filter :project_id, operator: '!', value: [project2.id]
+
+ sql_result = @query.result
+ expect(sql_result.count).to eq(2)
+ end
+
+ it 'should process only _one_ SQL query for any operations on a valid CostQuery' do
+ number_of_sql_queries = 0
+ expect_any_instance_of(CostQuery::SqlStatement).to receive(:to_s) do |*_|
+ number_of_sql_queries += 1 unless caller.third.include? 'sql_statement.rb'
+
+ # Apparently, we have to return a valid SQL query
+
+ 'SELECT 1=1'
+ end
+
+ # create a random query
+ @query.group_by :work_package_id
+ @query.column :tweek
+ @query.row :project_id
+ @query.row :user_id
+ # count how often a sql query was created
+ number_of_sql_queries = 0
+ # do some random things on it
+ walker = @query.transformer
+ walker.row_first
+ walker.column_first
+ # TODO - to do something
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/operator_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/operator_spec.rb
new file mode 100644
index 0000000000..fa7137a1de
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/operator_spec.rb
@@ -0,0 +1,270 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ minimal_query
+
+ let!(:project1) { FactoryGirl.create(:project, name: "project1", created_on: 5.minutes.ago) }
+ let!(:project2) { FactoryGirl.create(:project, name: "project2", created_on: 6.minutes.ago) }
+
+ describe CostQuery::Operator do
+ def query(table, field, operator, *values)
+ sql = CostQuery::SqlStatement.new table
+ yield sql if block_given?
+ operator.to_operator.modify sql, field, *values
+ ActiveRecord::Base.connection.select_all(sql.to_s).to_a
+ end
+
+ def query_on_entries(field, operator, *values)
+ sql = CostQuery::SqlStatement.for_entries
+ operator.to_operator.modify sql, field, *values
+ ActiveRecord::Base.connection.select_all(sql.to_s).to_a
+ end
+
+ def create_project(options = {})
+ parent = options.delete :parent
+ p = FactoryGirl.create(:project, options)
+ p.set_parent! parent if parent
+ p
+ end
+
+ it "does =" do
+ expect(query('projects', 'id', '=', project1.id).size).to eq(1)
+ end
+
+ it "does = for multiple values" do
+ expect(query('projects', 'id', '=', project1.id, project2.id).size).to eq(2)
+ end
+
+ it "does = for no values" do
+ sql = CostQuery::SqlStatement.new 'projects'
+ "=".to_operator.modify sql, 'id'
+ result = (ActiveRecord::Base.connection.select_all sql.to_s)
+ expect(result).to be_empty
+ end
+
+ it "does = for nil" do
+ expect(query('projects', 'id', '=', nil).size).to eq(0)
+ end
+
+ it "does <=" do
+ expect(query('projects', 'id', '<=', project2.id - 1).size).to eq(1)
+ end
+
+ it "does >=" do
+ expect(query('projects', 'id', '>=', project1.id + 1).size).to eq(1)
+ end
+
+ it "does !" do
+ expect(query('projects', 'id', '!', project1.id).size).to eq(1)
+ end
+
+ it "does ! for multiple values" do
+ expect(query('projects', 'id', '!', project1.id, project2.id).size).to eq(0)
+ end
+
+ it "does !*" do
+ expect(query('cost_entries', 'project_id', '!*', []).size).to eq(0)
+ end
+
+ it "does ~ (contains)" do
+ expect(query('projects', 'name', '~', 'o').size).to eq(Project.all.select { |p| p.name =~ /o/ }.count)
+ expect(query('projects', 'name', '~', 'test').size).to eq(Project.all.select { |p| p.name =~ /test/ }.count)
+ expect(query('projects', 'name', '~', 'child').size).to eq(Project.all.select { |p| p.name =~ /child/ }.count)
+ end
+
+ it "does !~ (not contains)" do
+ expect(query('projects', 'name', '!~', 'o').size).to eq(Project.all.select { |p| p.name !~ /o/ }.count)
+ expect(query('projects', 'name', '!~', 'test').size).to eq(Project.all.select { |p| p.name !~ /test/ }.count)
+ expect(query('projects', 'name', '!~', 'child').size).to eq(Project.all.select { |p| p.name !~ /child/ }.count)
+ end
+
+ it "does c (closed work_package)" do
+ expect(query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
+ end
+
+ it "does o (open work_package)" do
+ expect(query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size).to be >= 0
+ end
+
+ it "does give the correct number of results when counting closed and open work_packages" do
+ a = query('work_packages', 'status_id', 'o') { |s| s.join Status => [WorkPackage, :status] }.size
+ b = query('work_packages', 'status_id', 'c') { |s| s.join Status => [WorkPackage, :status] }.size
+ expect(WorkPackage.count).to eq(a + b)
+ end
+
+ it "does w (this week)" do
+ #somehow this test doesn't work on sundays
+ n = query('projects', 'created_on', 'w').size
+ day_in_this_week = Time.now.at_beginning_of_week + 1.day
+ FactoryGirl.create(:project, created_on: day_in_this_week)
+ expect(query('projects', 'created_on', 'w').size).to eq(n + 1)
+ FactoryGirl.create(:project, created_on: day_in_this_week + 7.days)
+ FactoryGirl.create(:project, created_on: day_in_this_week - 7.days)
+ expect(query('projects', 'created_on', 'w').size).to eq(n + 1)
+ end
+
+ it "does t (today)" do
+ s = query('projects', 'created_on', 't').size
+ FactoryGirl.create(:project, created_on: Date.yesterday)
+ expect(query('projects', 'created_on', 't').size).to eq(s)
+ FactoryGirl.create(:project, created_on: Time.now)
+ expect(query('projects', 'created_on', 't').size).to eq(s + 1)
+ end
+
+ it "does t+ (after the day which is n days in the furure)" do
+ n = query('projects', 'created_on', '>t+', 1).size
+ FactoryGirl.create(:project, created_on: Time.now)
+ expect(query('projects', 'created_on', '>t+', 1).size).to eq(n)
+ FactoryGirl.create(:project, created_on: Date.tomorrow + 1)
+ expect(query('projects', 'created_on', '>t+', 1).size).to eq(n + 1)
+ end
+
+ it "does >t- (after the day which is n days ago)" do
+ n = query('projects', 'created_on', '>t-', 1).size
+ FactoryGirl.create(:project, created_on: Date.today)
+ expect(query('projects', 'created_on', '>t-', 1).size).to eq(n + 1)
+ FactoryGirl.create(:project, created_on: Date.yesterday - 1)
+ expect(query('projects', 'created_on', '>t-', 1).size).to eq(n + 1)
+ end
+
+ it "does t- (n days ago)" do
+ n = query('projects', 'created_on', 't-', 1).size
+ FactoryGirl.create(:project, created_on: Date.yesterday)
+ expect(query('projects', 'created_on', 't-', 1).size).to eq(n + 1)
+ FactoryGirl.create(:project, created_on: Date.yesterday - 2)
+ expect(query('projects', 'created_on', 't-', 1).size).to eq(n + 1)
+ end
+
+ it "does d" do
+ expect(query('projects', 'created_on', '<>d', Time.now, 5.minutes.from_now).size).to eq(0)
+ end
+
+ it "does >d" do
+ #assuming that all projects were created in the past
+ expect(query('projects', 'created_on', '>d', Time.now).size).to eq(0)
+ end
+
+ describe 'arity' do
+ arities = {'t' => 0, 'w' => 0, '<>d' => 2, '>d' => 1}
+ arities.each do |o,a|
+ it("#{o} should take #{a} values") { expect(o.to_operator.arity).to eq(a) }
+ end
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/result_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/result_spec.rb
new file mode 100644
index 0000000000..8aed4ab97f
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/result_spec.rb
@@ -0,0 +1,147 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ before do
+ FactoryGirl.create(:admin)
+ project = FactoryGirl.create(:project_with_types)
+ work_package = FactoryGirl.create(:work_package, project: project)
+ FactoryGirl.create(:time_entry, work_package: work_package, project: project)
+ FactoryGirl.create(:cost_entry, work_package: work_package, project: project)
+ end
+
+ minimal_query
+
+ describe CostQuery::Result do
+ def direct_results(quantity = 0)
+ (1..quantity).map {|i| CostQuery::Result.new real_costs:i.to_f, count:1 ,units:i.to_f}
+ end
+
+ def wrapped_result(source, quantity=1)
+ CostQuery::Result.new((1..quantity).map { |i| source})
+ end
+
+ it "should travel recursively depth-first" do
+ #build a tree of wrapped and direct results
+ w1 = wrapped_result((direct_results 5), 3)
+ w2 = wrapped_result wrapped_result((direct_results 3), 2)
+ w = wrapped_result [w1, w2]
+ previous_depth = -1
+ w.recursive_each_with_level do |level, result|
+ #depth first, so we should get deeper into the hole, until we find a direct_result
+ expect(previous_depth).to eq(level - 1)
+ previous_depth=level
+ break if result.is_a? CostQuery::Result::DirectResult
+ end
+ end
+
+ it "should travel recursively width-first" do
+ #build a tree of wrapped and direct results
+ w1 = wrapped_result((direct_results 5), 3)
+ w2 = wrapped_result wrapped_result((direct_results 3), 2)
+ w = wrapped_result [w1, w2]
+
+ previous_depth = -1
+ w.recursive_each_with_level 0, false do |level, result|
+ #width first, so we should get only deeper into the hole without ever coming up again
+ expect(previous_depth).to be <= level
+ previous_depth=level
+ end
+ end
+
+ it "should travel to all results width-first" do
+ #build a tree of wrapped and direct results
+ w1 = wrapped_result((direct_results 5), 3)
+ w2 = wrapped_result wrapped_result((direct_results 3), 2)
+ w = wrapped_result [w1, w2]
+
+ count = 0
+ w.recursive_each_with_level 0, false do |level, result|
+ #width first
+ count = count + 1 if result.is_a? CostQuery::Result::DirectResult
+ end
+ expect(w.count).to eq(count)
+ end
+
+ it "should travel to all results width-first" do
+ #build a tree of wrapped and direct results
+ w1 = wrapped_result((direct_results 5), 3)
+ w2 = wrapped_result wrapped_result((direct_results 3), 2)
+ w = wrapped_result [w1, w2]
+
+ count = 0
+ w.recursive_each_with_level do |level, result|
+ #depth first
+ count = count + 1 if result.is_a? CostQuery::Result::DirectResult
+ end
+ expect(w.count).to eq(count)
+ end
+
+ it "should compute count correctly" do
+ expect(@query.result.count).to eq(Entry.count)
+ end
+
+ it "should compute units correctly" do
+ expect(@query.result.units).to eq(Entry.all.map { |e| e.units}.sum)
+ end
+
+ it "should compute real_costs correctly" do
+ expect(@query.result.real_costs).to eq(Entry.all.map { |e| e.overridden_costs || e.costs}.sum)
+ end
+
+ it "should compute count for DirectResults" do
+ expect(@query.result.values[0].count).to eq(1)
+ end
+
+ it "should compute units for DirectResults" do
+ id_sorted = @query.result.values.sort_by { |r| r[:id] }
+ te_result = id_sorted.select { |r| r[:type]==TimeEntry.to_s }.first
+ ce_result = id_sorted.select { |r| r[:type]==CostEntry.to_s }.first
+ expect(te_result.units.to_s).to eq("1.0")
+ expect(ce_result.units.to_s).to eq("1.0")
+ end
+
+ it "should compute real_costs for DirectResults" do
+ id_sorted = @query.result.values.sort_by { |r| r[:id] }
+ [CostEntry].each do |type|
+ result = id_sorted.select { |r| r[:type]==type.to_s }.first
+ first = type.all.first
+ expect(result.real_costs).to eq(first.overridden_costs || first.costs)
+ end
+ end
+
+ it "should be a column if created with CostQuery.column" do
+ @query.column :project_id
+ expect(@query.result.type).to eq(:column)
+ end
+
+ it "should be a row if created with CostQuery.row" do
+ @query.row :project_id
+ expect(@query.result.type).to eq(:row)
+ end
+
+ it "should show the type :direct for its direct results" do
+ @query.column :project_id
+ expect(@query.result.first.first.type).to eq(:direct)
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/validation_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/validation_spec.rb
new file mode 100644
index 0000000000..a481f35744
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/validation_spec.rb
@@ -0,0 +1,77 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe "CostQuery::Validation", type: :model do
+ class CostQuery::SomeBase
+ include CostQuery::Validation
+ end
+
+ it "should be valid with no validations whatsoever" do
+ obj = CostQuery::SomeBase.new
+ expect(obj.validate("foo")).to be_truthy
+ expect(obj.validations.size).to eq(0)
+ end
+
+ it "should allow for multiple validations" do
+ obj = CostQuery::SomeBase.new
+ obj.register_validations([:integers, :dates])
+ expect(obj.validations.size).to eq(2)
+ end
+
+ it "should have errors set when we try to validate something invalid" do
+ obj = CostQuery::SomeBase.new
+ obj.register_validation(:integers)
+ expect(obj.validate("this ain't a number, right?")).to be_falsey
+ expect(obj.errors[:int].size).to eq(1)
+ end
+
+ it "should have no errors set when we try to validate something valid" do
+ obj = CostQuery::SomeBase.new
+ obj.register_validation(:integers)
+ expect(obj.validate(1,2,3,4)).to be_truthy
+ expect(obj.errors[:int].size).to eq(0)
+ end
+
+ it "should validate integers correctly" do
+ obj = CostQuery::SomeBase.new
+ obj.register_validation(:integers)
+ expect(obj.validate(1,2,3,4)).to be_truthy
+ expect(obj.errors[:int].size).to eq(0)
+ expect(obj.validate("I ain't gonna work on Maggies Farm no more")).to be_falsey
+ expect(obj.errors[:int].size).to eq(1)
+ expect(obj.validate("You've got the touch!", "You've got the power!")).to be_falsey
+ expect(obj.errors[:int].size).to eq(2)
+ expect(obj.validate(1, "This is a good burger")).to be_falsey
+ expect(obj.errors[:int].size).to eq(1)
+ end
+
+ it "should validate dates correctly" do
+ obj = CostQuery::SomeBase.new
+ obj.register_validation(:dates)
+ expect(obj.validate("2010-04-15")).to be_truthy
+ expect(obj.errors[:date].size).to eq(0)
+ expect(obj.validate("2010-15-15")).to be_falsey
+ expect(obj.errors[:date].size).to eq(1)
+ expect(obj.validate("2010-04-31")).to be_falsey
+ expect(obj.errors[:date].size).to eq(1)
+ end
+
+end
diff --git a/vendored-plugins/openproject-reporting/spec/models/cost_query/walker_spec.rb b/vendored-plugins/openproject-reporting/spec/models/cost_query/walker_spec.rb
new file mode 100644
index 0000000000..7d5d9caa8f
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/models/cost_query/walker_spec.rb
@@ -0,0 +1,59 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe CostQuery, type: :model, reporting_query_helper: true do
+ minimal_query
+
+ before do
+ FactoryGirl.create(:admin)
+ project = FactoryGirl.create(:project_with_types)
+ work_package = FactoryGirl.create(:work_package, project: project)
+ FactoryGirl.create(:time_entry, work_package: work_package, project: project)
+ end
+
+ describe CostQuery::Transformer do
+ it "should walk down row_first" do
+ @query.group_by :work_package_id
+ @query.column :tweek
+ @query.row :project_id
+ @query.row :user_id
+
+ result = @query.transformer.row_first.values.first
+ [:user_id, :project_id, :tweek].each do |field|
+ expect(result.fields).to include(field)
+ result = result.values.first
+ end
+ end
+
+ it "should walk down column_first" do
+ @query.group_by :work_package_id
+ @query.column :tweek
+ @query.row :project_id
+ @query.row :user_id
+
+ result = @query.transformer.column_first.values.first
+ [:tweek, :work_package_id].each do |field|
+ expect(result.fields).to include(field)
+ result = result.values.first
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/requests/custom_field_cache_spec.rb b/vendored-plugins/openproject-reporting/spec/requests/custom_field_cache_spec.rb
new file mode 100644
index 0000000000..2540f6c1f8
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/requests/custom_field_cache_spec.rb
@@ -0,0 +1,122 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'spec_helper'
+require File.join(File.dirname(__FILE__), '..', 'support', 'custom_field_filter')
+require File.join(File.dirname(__FILE__), '..', 'support', 'configuration_helper')
+
+describe 'Custom field filter and group by caching', type: :request do
+ include OpenProject::Reporting::SpecHelper::CustomFieldFilterHelper
+ include OpenProject::Reporting::SpecHelper::ConfigurationHelper
+
+ let(:project) { FactoryGirl.create(:valid_project) }
+ let(:user) { FactoryGirl.create(:admin) }
+ let(:custom_field) { FactoryGirl.build(:work_package_custom_field) }
+ let(:custom_field2) { FactoryGirl.build(:work_package_custom_field) }
+
+ before do
+ allow(User).to receive(:current).and_return(user)
+
+ custom_field.save!
+ end
+
+ after do
+ CostQuery::Cache.reset!
+ end
+
+ def expect_group_by_all_to_include(custom_field)
+ expect(CostQuery::GroupBy.all).to include(group_by_class_name_string(custom_field).constantize)
+ end
+
+ def expect_filter_all_to_include(custom_field)
+ expect(CostQuery::Filter.all).to include(filter_class_name_string(custom_field).constantize)
+ end
+
+ def expect_group_by_all_to_not_exist(custom_field)
+ # can not check for whether the element is included in CostQuery::GroupBy.all if it does not exist
+ expect { group_by_class_name_string(custom_field).constantize }.to raise_error NameError
+ end
+
+ def expect_filter_all_to_not_exist(custom_field)
+ # can not check for whether the element is included in CostQuery::Filter.all if it does not exist
+ expect { filter_class_name_string(custom_field).constantize }.to raise_error NameError
+ end
+
+ def visit_cost_reports_index
+ get "/projects/#{project.id}/cost_reports"
+ end
+
+ it 'removes the filter/group_by if the custom field is removed' do
+ custom_field2.save!
+
+ visit_cost_reports_index
+
+ expect_group_by_all_to_include(custom_field)
+ expect_group_by_all_to_include(custom_field2)
+
+ expect_filter_all_to_include(custom_field)
+ expect_filter_all_to_include(custom_field2)
+
+ custom_field2.destroy
+
+ visit_cost_reports_index
+
+ expect_group_by_all_to_include(custom_field)
+ expect_group_by_all_to_not_exist(custom_field2)
+
+ expect_filter_all_to_include(custom_field)
+ expect_filter_all_to_not_exist(custom_field2)
+ end
+
+ it 'removes the filter/group_by if the last custom field is removed' do
+ visit_cost_reports_index
+
+ expect_group_by_all_to_include(custom_field)
+ expect_filter_all_to_include(custom_field)
+
+ custom_field.destroy
+
+ visit_cost_reports_index
+
+ expect_group_by_all_to_not_exist(custom_field)
+ expect_filter_all_to_not_exist(custom_field)
+ end
+
+ it 'allows for changing the db entries directly via SQL between requests \
+ if no caching is done (this could also mean switching dbs)' do
+ new_label = "our new label"
+ mock_cache_classes_setting_with(false)
+
+ visit_cost_reports_index
+
+ expect_group_by_all_to_include(custom_field)
+ expect_filter_all_to_include(custom_field)
+
+ CustomField::Translation.where(custom_field_id: custom_field.id)
+ .update_all(name: new_label)
+
+ visit_cost_reports_index
+
+ expect_group_by_all_to_include(custom_field)
+ expect_filter_all_to_include(custom_field)
+
+ expect(group_by_class_name_string(custom_field).constantize.label).to eql(new_label)
+ expect(filter_class_name_string(custom_field).constantize.label).to eql(new_label)
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/spec_helper.rb b/vendored-plugins/openproject-reporting/spec/spec_helper.rb
new file mode 100644
index 0000000000..97edcd4367
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/spec_helper.rb
@@ -0,0 +1,23 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+RAILS_ENV = "test" unless defined? RAILS_ENV
+
+require "spec_helper"
+Dir[File.dirname(__FILE__) + '/support/*.rb'].each { |file| require file }
diff --git a/vendored-plugins/openproject-reporting/spec/support/configuration_helper.rb b/vendored-plugins/openproject-reporting/spec/support/configuration_helper.rb
new file mode 100644
index 0000000000..0714ebbe68
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/support/configuration_helper.rb
@@ -0,0 +1,31 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::SpecHelper
+ module ConfigurationHelper
+ def mock_cache_classes_setting_with(value)
+ allow(OpenProject::Configuration).to receive(:[]).and_call_original
+ allow(OpenProject::Configuration).to receive(:[])
+ .with('cost_reporting_cache_filter_classes')
+ .and_return(value)
+ allow(OpenProject::Configuration).to receive(:cost_reporting_cache_filter_classes)
+ .and_return(value)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/support/custom_field_filter.rb b/vendored-plugins/openproject-reporting/spec/support/custom_field_filter.rb
new file mode 100644
index 0000000000..ce7aabb3c7
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/support/custom_field_filter.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting::SpecHelper
+ module CustomFieldFilterHelper
+ def group_by_class_name_string(custom_field)
+ id = custom_field.is_a?(ActiveRecord::Base) ? custom_field.id : custom_field
+
+ "CostQuery::GroupBy::CustomField#{id}"
+ end
+
+ def filter_class_name_string(custom_field)
+ id = custom_field.is_a?(ActiveRecord::Base) ? custom_field.id : custom_field
+
+ "CostQuery::Filter::CustomField#{id}"
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/support/plugin_spec_helper.rb b/vendored-plugins/openproject-reporting/spec/support/plugin_spec_helper.rb
new file mode 100644
index 0000000000..643c049f30
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/support/plugin_spec_helper.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module OpenProject::Reporting
+ module PluginSpecHelper
+ def is_member(project, user, permissions = [])
+ role = FactoryGirl.create(:role, permissions: permissions)
+
+ FactoryGirl.create(:member, project: project,
+ principal: user,
+ roles: [role])
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-reporting/spec/support/query_helper.rb b/vendored-plugins/openproject-reporting/spec/support/query_helper.rb
new file mode 100644
index 0000000000..bb1bef76f6
--- /dev/null
+++ b/vendored-plugins/openproject-reporting/spec/support/query_helper.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# OpenProject Reporting Plugin
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# need to define all the operators
+require 'cost_query/operator'
+
+module OpenProject::Reporting
+ module QueryHelper
+ def minimal_query
+ before do
+ @query = CostQuery.new
+ @query.send(:minimal_chain!)
+ end
+ end
+ end
+end
+
+RSpec.configure do |c|
+ c.extend OpenProject::Reporting::QueryHelper, reporting_query_helper: true
+end
diff --git a/vendored-plugins/openproject-themes-dark/CHANGELOG.md b/vendored-plugins/openproject-themes-dark/CHANGELOG.md
new file mode 100644
index 0000000000..29b26b4e6b
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+## 3.0.8
+
+* adapt plugin to new version scheme
+* `#2231` Enable separate hover effect for submenu toggle
+
+## 1.0.0.pre1
+
+* first version of a dark theme for OpenProject
+
diff --git a/vendored-plugins/openproject-themes-dark/app/assets/images/dark-theme/favicon.ico b/vendored-plugins/openproject-themes-dark/app/assets/images/dark-theme/favicon.ico
new file mode 100644
index 0000000000..cdb77ad18a
Binary files /dev/null and b/vendored-plugins/openproject-themes-dark/app/assets/images/dark-theme/favicon.ico differ
diff --git a/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark.css.sass b/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark.css.sass
new file mode 100644
index 0000000000..e93a9bd477
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark.css.sass
@@ -0,0 +1,12 @@
+//= require bundles/openproject-global
+//= require bundles/openproject-core-app
+
+//= require print
+//= require scm
+//= require top-shelf
+//= require openproject_plugins
+
+//= require_self
+
+@import dark/variables
+@import default
diff --git a/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark/_variables.css.sass b/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark/_variables.css.sass
new file mode 100644
index 0000000000..0a0bae2fa9
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/app/assets/stylesheets/dark/_variables.css.sass
@@ -0,0 +1,51 @@
+/*-- copyright
+ * OpenProject is a project management system.
+ * Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 3.
+ *
+ * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ * Copyright (C) 2006-2013 Jean-Philippe Lang
+ * Copyright (C) 2010-2013 the ChiliProject Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See doc/COPYRIGHT.rdoc for more details. ++*/
+
+$primary-color: #06799f
+$primary-color-dark: #056584
+$alternative-color: #30873c
+
+$main-menu-border-color: #053242
+$main-menu-border-width: 0px
+$main-menu-item-border-color: #053242
+$main-menu-item-border-width: 0px
+
+$main-menu-bg-color: #053242
+$main-menu-bg-hover-selected-background: #04232f
+$main-menu-font-color: white
+$main-menu-selected-hover-indicator-color: white
+
+$main-menu-sidebar-font-color: #FFFFFF
+$main-menu-sidebar-h3-color: #FFFFFF
+$main-menu-sidebar-link-color: #FFFFFF
+$main-menu-enable-toggle-highlighting: true
+
+$drop-down-unselected-font-color: #333333
+$drop-down-selected-font-color: #BBBBBB
+$drop-down-selected-bg-color: #053242
+
+$context-menu-font-color: #333333
diff --git a/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark.rb b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark.rb
new file mode 100644
index 0000000000..202b5dc7f0
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark.rb
@@ -0,0 +1,11 @@
+module OpenProject
+ module Themes
+ module Dark
+ require 'open_project/themes/dark/engine'
+
+ def self.assets_path
+ @assets_path ||= Engine.root.join('app/assets').to_s
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/all.rb b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/all.rb
new file mode 100644
index 0000000000..2fd20ce017
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/all.rb
@@ -0,0 +1,9 @@
+require 'open_project/themes/theme'
+
+module OpenProject::Themes::Dark
+ class DarkTheme < OpenProject::Themes::Theme
+ def assets_path
+ OpenProject::Themes::Dark.assets_path
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/engine.rb b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/engine.rb
new file mode 100644
index 0000000000..13af5f1b3c
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/engine.rb
@@ -0,0 +1,29 @@
+module OpenProject::Themes::Dark
+ class Engine < ::Rails::Engine
+ engine_name :openproject_themes_dark
+
+ initializer 'themes.dark.register_themes' do
+ ActiveSupport.on_load(:themes) do
+ require 'open_project/themes/dark/all'
+ end
+ end
+
+ config.to_prepare do
+ require 'redmine/plugin'
+ require 'open_project/themes/dark/version'
+
+ Redmine::Plugin.register 'Dark-Theme' do
+ name 'OpenProject Dark Themes'
+ author 'OpenProject GmbH'
+ description 'Custom dark theme for OpenProject'
+
+ url 'https://github.com/finnlabs/openproject-themes-dark'
+ author_url 'http://www.openproject.com'
+
+ version OpenProject::Themes::Dark::VERSION
+
+ requires_openproject ">= 4.0.0"
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/version.rb b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/version.rb
new file mode 100644
index 0000000000..52f806161f
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/lib/open_project/themes/dark/version.rb
@@ -0,0 +1,7 @@
+module OpenProject
+ module Themes
+ module Dark
+ VERSION = "5.0.10"
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-themes-dark/lib/openproject-themes-dark.rb b/vendored-plugins/openproject-themes-dark/lib/openproject-themes-dark.rb
new file mode 100644
index 0000000000..13a4603d5c
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/lib/openproject-themes-dark.rb
@@ -0,0 +1 @@
+require 'open_project/themes/dark'
diff --git a/vendored-plugins/openproject-themes-dark/openproject-themes-dark.gemspec b/vendored-plugins/openproject-themes-dark/openproject-themes-dark.gemspec
new file mode 100644
index 0000000000..9c257b5728
--- /dev/null
+++ b/vendored-plugins/openproject-themes-dark/openproject-themes-dark.gemspec
@@ -0,0 +1,19 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "open_project/themes/dark/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-themes-dark"
+ s.version = OpenProject::Themes::Dark::VERSION
+ s.authors = ["OpenProject GmbH"]
+ s.email = ["info@openproject.com"]
+ s.homepage = "https://community.openproject.org/projects/dark-theme"
+ s.summary = "Dark theme for OpenProject"
+ s.description = "Theme with more dark blue colors for your OpenProject installation"
+
+ s.files = Dir["{app,lib}/**/*", "Rakefile", "README.rdoc"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+end
diff --git a/vendored-plugins/openproject-webhooks/README.md b/vendored-plugins/openproject-webhooks/README.md
new file mode 100644
index 0000000000..05586d2ae4
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/README.md
@@ -0,0 +1,64 @@
+# OpenProject Webhooks Plugin
+
+`openproject-webhooks` is an OpenProject plugin, which adds a webhook API to OpenProject. Other plugins may build upon this plugin to implement their functionality.
+
+External services like [GitHub](https://github.com/finnlabs/openproject-github_integration) or Travis could be integrated with the help of this plugin.
+
+**Note:** This is an infrastructure-only plugin. With this plugin alone, you will not notice any difference in your OpenProject installation.
+
+## Installation
+
+This is an OpenProject plugin, thus we follow the usual OpenProject plugin installation mechanism.
+
+### Requirements
+
+* OpenProject version **3.1.0 or higher** ( or a current installation from the `dev` branch)
+
+### Plugin Installation
+
+Edit the `Gemfile.plugins` file in your openproject-installation directory to contain the following lines:
+
+
+gem "openproject-webhooks", :git => 'https://github.com/finnlabs/openproject-webhooks.git', :branch => 'stable'
+
+
+Then update your bundle with:
+
+
+bundle install
+
+
+and restart the OpenProject server.
+
+## Contact
+
+OpenProject is supported by its community members, both companies and individuals.
+
+Please find ways to contact us on the OpenProject [support page](https://www.openproject.org/support).
+
+## Contributing
+
+This OpenProject plugin is an open source project and we encourage you to help us out. We'd be happy if you do one of these things:
+
+* Create a new [work package in the Webhooks plugin project on openproject.org](https://www.openproject.org/projects/webhooks/work_packages) if you find a bug or need a feature
+* Help out other people on our [forums](https://www.openproject.org/projects/openproject/boards)
+* Help us [translate this plugin to more languages](https://www.openproject.org/projects/openproject/wiki/Translations)
+* Contribute code via GitHub Pull Requests, see our [contribution page](https://www.openproject.org/projects/openproject/wiki/Contribution) for more information
+
+## Community
+
+OpenProject is driven by an active group of open source enthusiasts: software engineers, project managers, creatives, and consultants. OpenProject is supported by companies as well as individuals. We share the vision to build great open source project collaboration software.
+The [OpenProject Foundation (OPF)](https://www.openproject.org/projects/openproject/wiki/OpenProject_Foundation) will give official guidance to the project and the community and oversees contributions and decisions.
+
+## Repository
+
+This repository contains two main branches:
+
+* `dev`: The main development branch. We try to keep it stable in the sense of all tests are passing, but we don't recommend it for production systems.
+* `stable`: Contains the latest stable release that we recommend for production use. Use this if you always want the latest version of this plugin.
+
+## License
+
+Copyright (C) 2014 the OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See [doc/COPYRIGHT.md](doc/COPYRIGHT.md) for details.
diff --git a/vendored-plugins/openproject-webhooks/app/controllers/webhooks_controller.rb b/vendored-plugins/openproject-webhooks/app/controllers/webhooks_controller.rb
new file mode 100644
index 0000000000..571c822e8d
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/app/controllers/webhooks_controller.rb
@@ -0,0 +1,40 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'json'
+
+class WebhooksController < ApplicationController
+ accept_key_auth :handle_hook
+
+ def api_request?
+ # OpenProject only allows API requests based on an Accept request header.
+ # Webhooks (at least GitHub) don't send an Accept header as they're not interested
+ # in any part of the response except the HTTP status code.
+ # Also handling requests with a application/json Content-Type as API requests
+ # should be safe regarding CSRF as browsers don't send forms as JSON.
+ super || request.content_type == "application/json"
+ end
+
+ def handle_hook
+ hook = OpenProject::Webhooks.find(params.require 'hook_name')
+
+ if hook
+ code = hook.handle(env, params, find_current_user)
+ head code.is_a?(Integer) ? code : 200
+ else
+ head :not_found
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/config/routes.rb b/vendored-plugins/openproject-webhooks/config/routes.rb
new file mode 100644
index 0000000000..4d2413d370
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/config/routes.rb
@@ -0,0 +1,20 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+OpenProject::Application.routes.draw do
+ scope "", as: "webhooks" do
+ post "webhooks/:hook_name" => 'webhooks#handle_hook'
+ get "webhooks/:hook_name" => 'webhooks#handle_hook'
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/doc/CHANGELOG.md b/vendored-plugins/openproject-webhooks/doc/CHANGELOG.md
new file mode 100644
index 0000000000..af70c3faef
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/doc/CHANGELOG.md
@@ -0,0 +1,18 @@
+
+
+# Changelog
+
+* `#` Create plugin
diff --git a/vendored-plugins/openproject-webhooks/doc/COPYRIGHT.md b/vendored-plugins/openproject-webhooks/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..3e6aac464e
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/doc/COPYRIGHT.md
@@ -0,0 +1,16 @@
+OpenProject is a project management system.
+
+Copyright (C) 2013 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/openproject-webhooks/doc/COPYRIGHT_short.md b/vendored-plugins/openproject-webhooks/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..01c04a91d2
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/doc/COPYRIGHT_short.md
@@ -0,0 +1,11 @@
+OpenProject is a project management system.
+Copyright (C) 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 3.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+See doc/COPYRIGHT.md for more details.
diff --git a/vendored-plugins/openproject-webhooks/doc/GPL.txt b/vendored-plugins/openproject-webhooks/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/openproject-webhooks/lib/open_project/webhooks.rb b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks.rb
new file mode 100644
index 0000000000..685d5e0479
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks.rb
@@ -0,0 +1,56 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module Webhooks
+ require "open_project/webhooks/engine"
+ require "open_project/webhooks/hook"
+
+ @@registered_hooks = []
+
+ ##
+ # Returns a list of currently active webhooks.
+ def self.registered_hooks
+ @@registered_hooks.dup
+ end
+
+ ##
+ # Registers a webhook having name and a callback.
+ # The name will be part of the webhook-url and may be used to unregister a webhook later.
+ # The callback is executed with two parameters when the webhook was called.
+ # The parameters are the hook object, an environment-variables hash and a params hash of the current request.
+ # The callback may return an Integer, which is interpreted as a http return code.
+ #
+ # Returns the newly created hook
+ def self.register_hook(name, &callback)
+ raise "A hook named '#{name}' is already registered!" if find(name)
+ Rails.logger.warn "hook registered"
+ hook = Hook.new(name, &callback)
+ @@registered_hooks << hook
+ hook
+ end
+
+ # Unregisters a webhook. Might be usefull for tests only, because routes can not
+ # be redrawn in a running instance
+ def self.unregister_hook(name)
+ hook = find(name)
+ raise "A hook named '#{name}' was not registered!" unless find(name)
+ @@registered_hooks.delete hook
+ end
+
+ def self.find(name)
+ @@registered_hooks.find {|h| h.name == name}
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/engine.rb b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/engine.rb
new file mode 100644
index 0000000000..4bb5baa13c
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/engine.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/plugins'
+
+module OpenProject::Webhooks
+ class Engine < ::Rails::Engine
+ engine_name :openproject_webhooks
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-webhooks',
+ :author_url => 'http://finn.de',
+ :requires_openproject => '>= 3.0.0pre43'
+
+ config.before_configuration do |app|
+ # This is required for the routes to be loaded first as the routes should
+ # be prepended so they take precedence over the core.
+ app.config.paths['config/routes.rb'].unshift File.join(File.dirname(__FILE__), "..", "..", "..", "config", "routes.rb")
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/hook.rb b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/hook.rb
new file mode 100644
index 0000000000..52034fef82
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/hook.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject::Webhooks
+ class Hook
+ attr_accessor :name, :callback
+
+ def initialize(name, &callback)
+ super()
+ @name = name
+ @callback = callback
+ end
+
+ def relative_url
+ "webhooks/#{name}"
+ end
+
+ def handle(environment = Hash.new, params = Hash.new, user = nil)
+ callback.call self, environment, params, user
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/version.rb b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/version.rb
new file mode 100644
index 0000000000..1ed2463c88
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/lib/open_project/webhooks/version.rb
@@ -0,0 +1,19 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+module OpenProject
+ module Webhooks
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/lib/openproject-webhooks.rb b/vendored-plugins/openproject-webhooks/lib/openproject-webhooks.rb
new file mode 100644
index 0000000000..337180425f
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/lib/openproject-webhooks.rb
@@ -0,0 +1,15 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'open_project/webhooks'
diff --git a/vendored-plugins/openproject-webhooks/openproject-webhooks.gemspec b/vendored-plugins/openproject-webhooks/openproject-webhooks.gemspec
new file mode 100644
index 0000000000..97366c2aef
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/openproject-webhooks.gemspec
@@ -0,0 +1,20 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/webhooks/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-webhooks"
+ s.version = OpenProject::Webhooks::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/webhooks"
+ s.summary = 'OpenProject Webhooks'
+ s.description = 'Provides a plug-in API to support OpenProject webhooks for better 3rd party integration'
+ s.license = 'GPLv3'
+
+ s.files = Dir["{app,config,db,doc,lib}/**/*"] + %w(README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+
+end
diff --git a/vendored-plugins/openproject-webhooks/spec/controllers/webhooks_controller_spec.rb b/vendored-plugins/openproject-webhooks/spec/controllers/webhooks_controller_spec.rb
new file mode 100644
index 0000000000..bee21aa802
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/spec/controllers/webhooks_controller_spec.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+
+describe WebhooksController, :type => :controller do
+ let(:hook) { double(OpenProject::Webhooks::Hook) }
+ let(:user) { double(User).as_null_object }
+
+ describe '#handle_hook' do
+ before do
+ expect(OpenProject::Webhooks).to receive(:find).with('testhook').and_return(hook)
+ allow(controller).to receive(:find_current_user).and_return(user)
+ end
+
+ after do
+ # ApplicationController before filter user_setup sets a user
+ User.current = nil
+ end
+
+ it 'should be successful' do
+ expect(hook).to receive(:handle)
+
+ post :handle_hook, :hook_name => 'testhook'
+
+ expect(response).to be_success
+ end
+
+ it 'should call the hook with a user' do
+ expect(hook).to receive(:handle) { |env, params, user|
+ expect(user).to equal(user)
+ }
+
+ post :handle_hook, :hook_name => 'testhook'
+ end
+
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/spec/lib/hook_spec.rb b/vendored-plugins/openproject-webhooks/spec/lib/hook_spec.rb
new file mode 100644
index 0000000000..98640de50e
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/spec/lib/hook_spec.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+
+describe OpenProject::Webhooks::Hook do
+ describe '#relative_url' do
+ let(:hook) { OpenProject::Webhooks::Hook.new('myhook')}
+
+ it "should return the correct URL" do
+ expect(hook.relative_url).to eql('webhooks/myhook')
+ end
+ end
+
+ describe '#handle' do
+ let(:probe) { lambda{} }
+ let(:hook) { OpenProject::Webhooks::Hook.new('myhook', &probe) }
+
+ before do
+ expect(probe).to receive(:call).with(hook, 1, 2, 3)
+ end
+
+ it 'should execute the callback with the correct parameters' do
+ hook.handle(1, 2, 3)
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-webhooks/spec/lib/webhooks_spec.rb b/vendored-plugins/openproject-webhooks/spec/lib/webhooks_spec.rb
new file mode 100644
index 0000000000..21217d6a98
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/spec/lib/webhooks_spec.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require File.expand_path('../../spec_helper', __FILE__)
+
+
+describe OpenProject::Webhooks do
+ describe '.register_hook' do
+ after do
+ OpenProject::Webhooks.unregister_hook('testhook1')
+ end
+
+ it 'should succeed' do
+ OpenProject::Webhooks.register_hook('testhook1') {}
+ end
+ end
+
+ describe '.find' do
+ let!(:hook) { OpenProject::Webhooks.register_hook('testhook3') {} }
+
+ after do
+ OpenProject::Webhooks.unregister_hook('testhook3')
+ end
+
+ it 'should succeed' do
+ expect(OpenProject::Webhooks.find('testhook3')).to equal(hook)
+ end
+ end
+
+ describe '.unregister_hook' do
+ let(:probe) { lambda{} }
+
+ before do
+ OpenProject::Webhooks.register_hook('testhook2', &probe)
+
+ end
+
+ it 'should result in the hook no longer being found' do
+ OpenProject::Webhooks.unregister_hook('testhook2')
+ expect(OpenProject::Webhooks.find('testhook2')).to be_nil
+ end
+ end
+
+end
diff --git a/vendored-plugins/openproject-webhooks/spec/spec_helper.rb b/vendored-plugins/openproject-webhooks/spec/spec_helper.rb
new file mode 100644
index 0000000000..be3099880b
--- /dev/null
+++ b/vendored-plugins/openproject-webhooks/spec/spec_helper.rb
@@ -0,0 +1,15 @@
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See doc/COPYRIGHT.md for more details.
+#++
+
+require 'spec_helper'
diff --git a/vendored-plugins/openproject-xls_export/CHANGELOG.md b/vendored-plugins/openproject-xls_export/CHANGELOG.md
new file mode 100644
index 0000000000..41cf7ae494
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Changelog
+
+## 3.0.8
+## 1.0.0.pre5
+
+* `#2561` [Work package tracking] Internal error when exporting work package list with cost columns to excel
+* `#2755` Internal error when exporting empty work package list
+
+## 1.0.0.pre4
+
+* `#2267` Rename view issue hooks
+
+## 1.0.0.pre3
+
+* Set Gem homepage to openproject.org project
+* Rename issue to work_package
+
+## 1.0.0.pre2
+
+* `#1907` Adapt XLS export to WorkPackagesController.index
+
+## 1.0.0.pre1
+
+* `#1476` Migration Rails3 Export
diff --git a/vendored-plugins/openproject-xls_export/README.md b/vendored-plugins/openproject-xls_export/README.md
new file mode 100644
index 0000000000..ddf3b250d3
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/README.md
@@ -0,0 +1,9 @@
+# OpenProject XLS Export Plugin
+
+Export issue lists as Excel spreadsheets (.xls). This plugin adds a link below each issue list which allows downloading the list as Excel spreadsheet.
+
+Support for exporting cost entries and cost reports is not yet migrated to Rails 3 and disabled.
+
+# Issue Tracker
+
+https://www.openproject.org/projects/export/issues
diff --git a/vendored-plugins/openproject-xls_export/app/views/hooks-disabled/xls_report/_view_cost_report_other_formats.rhtml b/vendored-plugins/openproject-xls_export/app/views/hooks-disabled/xls_report/_view_cost_report_other_formats.rhtml
new file mode 100644
index 0000000000..78db1e3264
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/app/views/hooks-disabled/xls_report/_view_cost_report_other_formats.rhtml
@@ -0,0 +1,7 @@
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'excel_export', :plugin => 'redmine_additional_formats' %>
+<% end %>
+<% if User.current.allowed_to? :export_issues, @project, :global => @project.nil? %>
+ <%= link_to(l(:export_to_excel), { :controller => "cost_reports" , :action => :index,
+ :format => 'xls', :project_id => @project }, :class => "icon icon-export-xls-descr") %>
+<% end %>
diff --git a/vendored-plugins/openproject-xls_export/config/locales/de.yml b/vendored-plugins/openproject-xls_export/config/locales/de.yml
new file mode 100644
index 0000000000..34c369371c
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/config/locales/de.yml
@@ -0,0 +1,6 @@
+de:
+ print_with_description: "Druckvorschau mit Beschreibung"
+ export_to_excel: "Exportieren als Excel Spreadsheet"
+ sentence_separator_or: "oder"
+ different_formats: Andere Formate
+ xls_with_descriptions: XLS mit Beschreibungen
\ No newline at end of file
diff --git a/vendored-plugins/openproject-xls_export/config/locales/en.yml b/vendored-plugins/openproject-xls_export/config/locales/en.yml
new file mode 100644
index 0000000000..3db7b8e6b0
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/config/locales/en.yml
@@ -0,0 +1,6 @@
+en:
+ export_to_excel: "Export as Excel spreadsheet"
+ print_with_description: "Print preview with description"
+ sentence_separator_or: "or"
+ different_formats: Different formats
+ xls_with_descriptions: XLS with Descriptions
\ No newline at end of file
diff --git a/vendored-plugins/openproject-xls_export/config/locales/js-de.yml b/vendored-plugins/openproject-xls_export/config/locales/js-de.yml
new file mode 100644
index 0000000000..2a1c8a9142
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/config/locales/js-de.yml
@@ -0,0 +1,4 @@
+de:
+ js:
+ label_format_xls: "XLS"
+ label_format_xls_with_descriptions: "XLS mit Beschreibungen"
diff --git a/vendored-plugins/openproject-xls_export/config/locales/js-en.yml b/vendored-plugins/openproject-xls_export/config/locales/js-en.yml
new file mode 100644
index 0000000000..88417a8007
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/config/locales/js-en.yml
@@ -0,0 +1,4 @@
+en:
+ js:
+ label_format_xls: "XLS"
+ label_format_xls_with_descriptions: "XLS with descriptions"
diff --git a/vendored-plugins/openproject-xls_export/features/step_definitions/format_steps.rb b/vendored-plugins/openproject-xls_export/features/step_definitions/format_steps.rb
new file mode 100644
index 0000000000..a62f94bfa2
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/features/step_definitions/format_steps.rb
@@ -0,0 +1,7 @@
+Then /^there should be a link to the work package list in ([^ ]+) format( with descriptions)?$/ do |format, with_descriptions|
+ path_options = {:project_id => Project.first.identifier, :format => 'xls'}
+ path_options[:show_descriptions] = 'true' if with_descriptions
+ # Use XPath to match both title and URL, otherwise we might get ambiguous matches
+ find(:xpath, "//a[contains(., '#{format}') and " +
+ "@href = '#{project_work_packages_path(path_options)}']")
+end
diff --git a/vendored-plugins/openproject-xls_export/features/xls_export_link.feature b/vendored-plugins/openproject-xls_export/features/xls_export_link.feature
new file mode 100644
index 0000000000..7a7affd19e
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/features/xls_export_link.feature
@@ -0,0 +1,9 @@
+Feature: Show link to XLS format below work package list
+
+ @wip
+ Scenario: There is a link to the work package list in XML format
+ Given there is a project named "Test Project"
+ And I am already admin
+ When I go to the work packages index page for the project "Test Project"
+ Then there should be a link to the work package list in XLS format
+ And there should be a link to the work package list in XLS format with descriptions
diff --git a/vendored-plugins/openproject-xls_export/frontend/app/openproject-xls_export-app.js b/vendored-plugins/openproject-xls_export/frontend/app/openproject-xls_export-app.js
new file mode 100644
index 0000000000..d880e0ad44
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/frontend/app/openproject-xls_export-app.js
@@ -0,0 +1,6 @@
+// load all js locales
+var localeFiles = require.context('../../config/locales', false, /js-[\w|-]{2,5}\.yml$/);
+localeFiles.keys().forEach(function(localeFile) {
+ var locale = localeFile.match(/js-([\w|-]{2,5})\.yml/)[1];
+ I18n.addTranslations(locale, localeFiles(localeFile)[locale]);
+});
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export.rb
new file mode 100644
index 0000000000..c85343c7c5
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module XlsExport
+ require "open_project/xls_export/engine"
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/hooks/cost_report_hook.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/hooks/cost_report_hook.rb
new file mode 100644
index 0000000000..7be9563f94
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/hooks/cost_report_hook.rb
@@ -0,0 +1,7 @@
+# Hooks to attach to the Redmine Issues.
+module XlsReport
+ class CostReportHook < Redmine::Hook::ViewListener
+ # Renders the Cost Object subject and basic costs information
+ render_on :view_cost_report_table_bottom, :partial => 'hooks/xls_report/view_cost_report_other_formats'
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/patches/cost_reports_controller_patch.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/patches/cost_reports_controller_patch.rb
new file mode 100644
index 0000000000..49b382837b
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/patches/cost_reports_controller_patch.rb
@@ -0,0 +1,61 @@
+# Only apply this patch if the redmine_costs plugin is available
+if require_dependency 'cost_reports_controller'
+ require_dependency 'xls_report/spreadsheet_builder'
+ require_dependency 'xls_report/xls_views'
+
+ module XlsReport
+ module CostReportsControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+ base.class_eval do
+
+ alias_method_chain :ensure_project_scope?, :excel_export
+ end
+ end
+
+ module InstanceMethods
+
+ def excel_export?
+ (params["action"] == "index" or params[:action] == "all") && params["format"] == "xls"
+ end
+
+ def ensure_project_scope_with_excel_export?
+ !excel_export? && ensure_project_scope_without_excel_export?
+ end
+
+ # If the index action is called, hook the xls format into the cost report
+ def respond_to
+ if excel_export?
+ super do |format|
+ yield format
+ format.xls do
+ report = report_to_xls
+ time = DateTime.now.strftime('%d-%m-%Y-T-%H-%M-%S')
+ send_data(report, :type => :xls, :filename => "export-#{time}.xls") if report
+ end
+ end
+ else
+ super
+ end
+ end
+
+ # Build an xls file from a cost report.
+ def report_to_xls
+ options = { :query => @query, :project => @project, :cost_types => @cost_types }
+
+ if @query.group_bys.empty?
+ sb = CostEntryTable.generate(options)
+ elsif @query.depth_of(:column) + @query.depth_of(:row) == 1
+ sb = SimpleCostReportTable.generate(options)
+ else
+ sb = CostReportTable.generate(options)
+ end
+ sb.xls
+ end
+ end
+ end
+ end
+
+ CostReportsController.send(:include, XlsReport::CostReportsControllerPatch)
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views.rb
new file mode 100644
index 0000000000..e72da440f0
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views.rb
@@ -0,0 +1,61 @@
+class XlsViews
+ include Redmine::I18n
+ include ActionView::Helpers::NumberHelper
+ include ApplicationHelper
+ include ReportingHelper
+ attr_accessor :spreadsheet, :query, :cost_type, :unit_id, :options
+
+ # Overwrite a few mappings.
+ def field_representation_map(key, value)
+ case key.to_sym
+ when :units then value.to_i
+ when :spent_on then value
+ when :activity_id then mapped value, Enumeration, l(:caption_material_costs)
+ when :project_id then (l(:label_none) if value.to_i == 0) or Project.find(value.to_i).name
+ when :user_id, :assigned_to_id then (l(:label_none) if value.to_i == 0) or User.find(value.to_i).name
+ when :issue_id
+ return l(:label_none) if value.to_i == 0
+ issue = Issue.find(value.to_i)
+ "#{issue.project.name + " - " if @project}#{issue.tracker} ##{issue.id}: #{issue.subject}"
+ else super(key, value)
+ end
+ end
+
+ def show_result(row, unit_id = @unit_id, as_text = false)
+ return super(row, unit_id) if as_text
+ case unit_id
+ when 0 then row.real_costs ? row.real_costs : '-'
+ else row.units
+ end
+ end
+
+ def cost_type_unit_label(cost_type_id, cost_type_inst = nil, plural = true)
+ case cost_type_id
+ when -1 then l_hours(2).split[1..-1].join(" ") # get the plural for hours
+ when 0 then Setting.plugin_redmine_costs['costs_currency']
+ else cost_type_label(cost_type_id, cost_type_inst, plural)
+ end
+ end
+
+ def serialize_query_without_hidden(query)
+ serialized_query = query.serialize
+ serialized_query[:filters] = serialized_query[:filters].reject do |name, options|
+ options[:display] == false
+ end
+ serialized_query
+ end
+
+ def self.generate(opts)
+ self.new.tap do |obj|
+ obj.query = opts[:query]
+ obj.cost_type = opts[:cost_type]
+ obj.unit_id = opts[:unit_id]
+ obj.options = opts
+ end.generate
+ end
+end
+
+# Load subclasses
+require_dependency 'xls_report/xls_views/cost_entry_table.xls'
+require_dependency 'xls_report/xls_views/simple_cost_report_table.xls'
+require_dependency 'xls_report/xls_views/cost_report_table.xls'
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_entry_table.xls.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_entry_table.xls.rb
new file mode 100644
index 0000000000..645c8a6037
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_entry_table.xls.rb
@@ -0,0 +1,59 @@
+require_dependency 'xls_report/xls_views'
+
+class CostEntryTable < XlsViews
+ def generate
+ spreadsheet = SpreadsheetBuilder.new(l(:label_money))
+ default_query = serialize_query_without_hidden(@query)
+
+ available_cost_type_tabs(options[:cost_types]).each_with_index do |ary, idx|
+ @query = CostQuery.deserialize(default_query)
+ @cost_type = nil
+ @unit_id = ary.first
+ name = ary.last
+
+ if @unit_id != 0
+ @query.filter :cost_type_id, :operator => '=', :value => @unit_id.to_s
+ @cost_type = CostType.find(unit_id) if unit_id > 0
+ end
+
+ spreadsheet.worksheet(idx, name)
+ build_spreadsheet(spreadsheet)
+ end
+ spreadsheet
+ end
+
+ def build_spreadsheet(spreadsheet)
+ spreadsheet.add_title("#{@project.name + " >> " if @project}#{l(:cost_reports_title)} (#{format_date(Date.today)})")
+
+ list = [:spent_on, :user_id, :activity_id, :issue_id, :comments, :project_id]
+ headers = list.collect {|field| label_for(field) }
+ headers << l(:units)
+ headers << l(:field_cost_type)
+ headers << l(:field_costs)
+ spreadsheet.add_headers(headers)
+
+ spreadsheet.add_format_option_to_column(headers.length - 1, :number_format => number_to_currency(0.00))
+ spreadsheet.add_format_option_to_column(headers.length - 2, :number_format => "0.0")
+
+ query.each_direct_result do |result|
+ row = list.collect {|field| show_field field, result.fields[field.to_s] }
+ current_cost_type_id = result.fields['cost_type_id'].to_i
+
+ row << show_result(result, current_cost_type_id) # units
+ row << cost_type_label(current_cost_type_id, @cost_type) # cost type
+ row << show_result(result, 0) # costs/currency
+
+ spreadsheet.add_row(row)
+ end
+
+ footer = [''] * list.size
+ if show_result(query, 0) != show_result(query)
+ footer += [show_result(query), '', show_result(query, 0)]
+ else
+ footer += ['', '', show_result(query)]
+ end
+ spreadsheet.add_row(footer) # footer
+
+ spreadsheet
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_report_table.xls.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_report_table.xls.rb
new file mode 100644
index 0000000000..9bba40ad2d
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/cost_report_table.xls.rb
@@ -0,0 +1,136 @@
+require_dependency 'xls_report/xls_views'
+
+class CostReportTable < XlsViews
+ def final_row(final_row, cells)
+ row = [show_row final_row]
+ row += cells
+ row << show_result(final_row)
+ end
+
+ def row(row, subrows)
+ unless row.fields.empty?
+ # Here we get the border setting, vertically. The rowspan #{subrows.size} need be
+ # converted to a proper Excel bordering
+ subrows = subrows.inject([]) do |array, subrow|
+ if subrow.flatten == subrow
+ array << subrow
+ else
+ array += subrow.collect(&:flatten)
+ end
+ end
+ subrows.each_with_index do |subrow, idx|
+ if idx == 0
+ subrow.insert(0, show_row(row))
+ subrow << show_result(row)
+ else
+ subrow.unshift("")
+ subrow << ""
+ end
+ end
+ end
+ subrows
+ end
+
+ def cell(result)
+ show_result result
+ end
+
+ def headers(list, first, first_in_col, last_in_col)
+ if first_in_col # Open a new header row
+ @header = [""] * query.depth_of(:row) # TODO: needs borders: rowspan=query.depth_of(:column)
+ end
+
+ list.each do |column|
+ @header << show_row(column)
+ @header += [""] * (column.final_number(:column) - 1).abs
+ end
+
+ if last_in_col # Finish this header row
+ @header += [""] * query.depth_of(:row) # TODO: needs borders: rowspan=query.depth_of(:column)
+ @headers << @header
+ end
+ end
+
+ def footers(list, first, first_in_col, last_in_col)
+ if first_in_col # Open a new footer row
+ @footer = [""] * query.depth_of(:row) # TODO: needs borders: rowspan=query.depth_of(:column)
+ end
+
+ list.each do |column|
+ @footer << show_result(column)
+ @footer += [""] * (column.final_number(:column) - 1).abs
+ end
+
+ if last_in_col # Finish this footer row
+ if first
+ @footer << show_result(query)
+ @footer += [""] * (query.depth_of(:row) - 1).abs # TODO: add rowspan=query.depth_of(:column)
+ else
+ @footer += [""] * query.depth_of(:row) # TODO: add rowspan=query.depth_of(:column)
+ end
+ @footers << @footer
+ end
+ end
+
+ def body(*line)
+ @rows += line.flatten
+ end
+
+ def generate
+ @spreadsheet ||= SpreadsheetBuilder.new(l(:label_money))
+ default_query = serialize_query_without_hidden(@query)
+
+ available_cost_type_tabs(options[:cost_types]).each_with_index do |ary, idx|
+ @query = CostQuery.deserialize(default_query)
+ @unit_id = ary.first
+ name = ary.last
+
+ if @unit_id != 0
+ @query.filter :cost_type_id, :operator => '=', :value => @unit_id.to_s
+ @cost_type = CostType.find(unit_id) if unit_id > 0
+ end
+
+ spreadsheet.worksheet(idx, name)
+ run_walker
+ build_spreadsheet unless (@headers + @footers).empty?
+ end
+ spreadsheet
+ end
+
+ def run_walker
+ walker = query.walker
+ walker.for_final_row &method(:final_row)
+ walker.for_row &method(:row)
+ walker.for_empty_cell { "" }
+ walker.for_cell &method(:cell)
+
+ @headers = []
+ @header = []
+ walker.headers &method(:headers)
+
+ @footers = []
+ @footer = []
+ walker.reverse_headers &method(:footers)
+
+ @rows = []
+ walker.body &method(:body)
+ end
+
+ def build_spreadsheet
+ spreadsheet.add_title("#{@project.name + " >> " if @project}#{l(:cost_reports_title)} (#{format_date(Date.today)})")
+ spreadsheet.add_headers [label]
+ row_length = @headers.first.length
+ @headers.each {|head| spreadsheet.add_headers(head, spreadsheet.current_row) }
+ @rows.in_groups_of(row_length).each {|body| spreadsheet.add_row(body) }
+ @footers.each {|foot| spreadsheet.add_headers(foot, spreadsheet.current_row) }
+ spreadsheet
+ end
+
+ def label
+ "#{l(:caption_cost_type)}: " + case unit_id
+ when -1 then l(:field_hours)
+ when 0 then "EUR"
+ else cost_type.unit_plural
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/simple_cost_report_table.xls.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/simple_cost_report_table.xls.rb
new file mode 100644
index 0000000000..346e303a9f
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/disabled/xls_views/simple_cost_report_table.xls.rb
@@ -0,0 +1,54 @@
+require_dependency 'xls_report/xls_views'
+
+class SimpleCostReportTable < XlsViews
+ def generate
+ spreadsheet = SpreadsheetBuilder.new(l(:label_money))
+ default_query = serialize_query_without_hidden(@query)
+
+ available_cost_type_tabs(options[:cost_types]).each_with_index do |ary, idx|
+ @query = CostQuery.deserialize(default_query)
+ @cost_type = nil
+ @unit_id = ary.first
+ name = ary.last
+
+ if @unit_id != 0
+ @query.filter :cost_type_id, :operator => '=', :value => @unit_id.to_s
+ @cost_type = CostType.find(unit_id) if unit_id > 0
+ end
+
+ spreadsheet.worksheet(idx, name)
+ build_spreadsheet(spreadsheet)
+ end
+ spreadsheet
+ end
+
+ def build_spreadsheet(spreadsheet)
+ spreadsheet.add_title("#{@project.name + " >> " if @project}#{l(:cost_reports_title)} (#{format_date(Date.today)})")
+
+ list = query.collect {|r| r.important_fields }.flatten.uniq
+ show_units = list.include? "cost_type_id"
+ headers = list.collect {|field| label_for(field) }
+ headers << label_for(:field_units) << "" if show_units
+ headers << label_for(:label_sum) << ""
+ spreadsheet.add_headers(headers)
+
+ column = 0
+ spreadsheet.add_format_option_to_column(headers.length - (column += 1), :number_format => number_to_currency(0.00))
+ spreadsheet.add_format_option_to_column(headers.length - (column += 1), :number_format => "0.0 ?") if show_units
+
+ query.each do |result|
+ current_cost_type_id = result.fields[:cost_type_id].to_i
+ row = [show_row(result)]
+ row << show_result(result, current_cost_type_id) if show_units
+ row << cost_type_unit_label(current_cost_type_id, @cost_type) if show_units
+ row << show_result(result)
+ row << cost_type_unit_label(@unit_id, @cost_type)
+ spreadsheet.add_row(row)
+ end
+
+ footer = [''] * list.size
+ footer += ['', ''] if show_units
+ spreadsheet.add_row(footer + [show_result(query), cost_type_unit_label(@unit_id, @cost_type)]) # footer
+ spreadsheet
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/engine.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/engine.rb
new file mode 100644
index 0000000000..9aa8ddcd7f
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/engine.rb
@@ -0,0 +1,29 @@
+module OpenProject::XlsExport
+ class Engine < ::Rails::Engine
+ engine_name :openproject_xls_export
+
+ include OpenProject::Plugins::ActsAsOpEngine
+
+ register 'openproject-xls_export',
+ :author_url => 'http://finn.de/',
+ :requires_openproject => '>= 4.0.0'
+
+ patches [:WorkPackagesController, :QueryColumn]
+ # disabled since not yet migrated: :CostReportsController
+
+ initializer 'xls_export.register_hooks' do
+ # don't use require_dependency to not reload hooks in development mode
+
+ # disabled since not yet migrated
+ # require 'open_project/xls_export/hooks/cost_report_hook.rb'
+
+ require 'open_project/xls_export/hooks/work_package_hook.rb'
+ end
+
+ initializer 'xls_export.register_mimetypes' do
+ Mime::Type.register('application/vnd.ms-excel',
+ :xls,
+ %w(application/vnd.ms-excel)) unless defined? Mime::XLS
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/filename_helper.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/filename_helper.rb
new file mode 100644
index 0000000000..3d140027ab
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/filename_helper.rb
@@ -0,0 +1,9 @@
+module OpenProject::XlsExport
+ class FilenameHelper
+ # Remove characters that could cause problems on popular OSses
+ # => A string that does not start with a space or dot and does not contain any of \/:*?"<>|
+ def self.sane_filename(str)
+ str.gsub(/^[ \.]/,"").gsub(/[\\\/:\*\?"<>|"]/, "_")
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/formatters.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/formatters.rb
new file mode 100644
index 0000000000..1cb60147da
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/formatters.rb
@@ -0,0 +1,72 @@
+module OpenProject::XlsExport
+ module Formatters
+ def self.all
+ self.constants.map do |const|
+ Kernel.const_get("OpenProject::XlsExport::Formatters::#{const}")
+ end.select do |const|
+ const.is_a?(Class) && const != DefaultFormatter
+ end + [DefaultFormatter]
+ end
+
+ def self.keys
+ all.map(&:key)
+ end
+
+ ##
+ # Returns a Hash mapping columns to formatters to be used.
+ def self.for_columns(columns)
+ formatters = self.all
+ entries = columns.map do |column|
+ formatter = formatters.find { |formatter| formatter.apply? column }
+ [column, (formatter || DefaultFormatter).new]
+ end
+ Hash[entries]
+ end
+
+ class DefaultFormatter
+ ##
+ # Takes a QueryColumn and returns true if this formatter should be used to handle it.
+ def self.apply?(column)
+ column.xls_formatter == self.key
+ end
+
+ def self.key
+ name = self.name.demodulize.underscore
+ name[0..(name.index("_") - 1)].to_sym
+ end
+
+ ##
+ # Takes a WorkPackage and a QueryColumn and returns the value to be exported.
+ def format(work_package, column)
+ column.xls_value work_package
+ end
+
+ ##
+ # Takes a QueryColumn and returns format options for it.
+ def format_options(column)
+ {}
+ end
+ end
+
+ class TimeFormatter < DefaultFormatter
+ def format_options(column)
+ {:number_format => '0.0 "h"'}
+ end
+ end
+
+ class CostFormatter < DefaultFormatter
+ def format_options(column)
+ {:number_format => number_format_string}
+ end
+
+ def number_format_string
+ # [$CUR] makes sure we have an actually working currency format with arbitrary currencies
+ curr = "[$CUR]".gsub "CUR", ERB::Util.h(Setting.plugin_openproject_costs['costs_currency'])
+ format = ERB::Util.h Setting.plugin_openproject_costs['costs_currency_format']
+ number = '#,##0.00'
+
+ format.gsub("%n", number).gsub("%u", curr)
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/hooks/work_package_hook.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/hooks/work_package_hook.rb
new file mode 100644
index 0000000000..a8443c6749
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/hooks/work_package_hook.rb
@@ -0,0 +1,13 @@
+module PrintableIssues
+ class IssueHook < Redmine::Hook::ViewListener
+ # Add XLS format link below issue list
+ def view_work_packages_index_other_formats(context)
+ (context[:link_formatter].link_to 'XLS', :url => { :project_id => context[:project] }) +
+ ' ' +
+ (context[:link_formatter].link_to I18n.t(:xls_with_descriptions),
+ :url => { :project_id => context[:project],
+ :show_descriptions => true,
+ :format => 'xls' })
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/query_column_patch.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/query_column_patch.rb
new file mode 100644
index 0000000000..2bc47f5db6
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/query_column_patch.rb
@@ -0,0 +1,22 @@
+module OpenProject::XlsExport::Patches::QueryColumnPatch
+ def self.included(base) # :nodoc:
+ base.class_eval do
+ include InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ ##
+ # Returns the key (symbol) of the formatter to be used.
+ # E.g. :default, :time, :cost
+ #
+ # Available keys: OpenProject::XlsExport::Formatters.keys
+ def xls_formatter
+ nil
+ end
+
+ def xls_value(work_package)
+ value work_package
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/work_packages_controller_patch.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/work_packages_controller_patch.rb
new file mode 100644
index 0000000000..c3da9bed47
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/patches/work_packages_controller_patch.rb
@@ -0,0 +1,70 @@
+module OpenProject::XlsExport
+ module Patches
+ module WorkPackagesControllerPatch
+ def self.included(base) # :nodoc:
+ base.send(:include, InstanceMethods)
+
+
+ base.class_eval do
+ end
+ end
+
+ module InstanceMethods
+
+ # If the index action is called, hook the xls format into the issues controller
+ def respond_to(&block)
+ if ((params["action"] && params["action"].to_sym == :index or params[:action] == "all") && params["format"].to_s.downcase == "xls")
+ super do |format|
+ yield format
+ format.xls do
+ @issues = @query.results(:include => [:assigned_to, :type, :priority, :category, :fixed_version],
+ :order => sort_clause).work_packages
+ send_data(issues_to_xls(:show_descriptions => params[:show_descriptions]),
+ :type => "application/vnd.ms-excel",
+ :filename => FilenameHelper.sane_filename(
+ "#{Setting.app_title} #{I18n.t(:label_work_package_plural)} " +
+ "#{format_time_as_date(Time.now, '%Y-%m-%d')}.xls"))
+ end
+ end
+ else
+ super(&block)
+ end
+ end
+
+ # Convert an issues query with associated issues to xls using the queries columns as headers
+ def build_spreadsheet(project, issues, query, options)
+ columns = query.columns
+
+ sb = SpreadsheetBuilder.new("#{I18n.t(:label_work_package_plural)}")
+ formatters = OpenProject::XlsExport::Formatters.for_columns(columns)
+
+ headers = columns.collect(&:caption)
+ headers << WorkPackage.human_attribute_name(:description) if options[:show_descriptions]
+ sb.add_headers headers, 0
+
+ issues.each do |work_package|
+ row = (columns.collect do |column|
+ cv = formatters[column].format work_package, column
+ cv = cv.in_time_zone(current_user.time_zone) if cv.is_a?(ActiveSupport::TimeWithZone)
+ (cv.respond_to? :name) ? cv.name : cv
+ end)
+ row << work_package.description if options[:show_descriptions]
+ sb.add_row(row)
+ end
+
+ columns.each_with_index do |column, i|
+ options = formatters[column].format_options column
+ sb.add_format_option_to_column i, options
+ end
+
+ sb
+ end
+
+ # Return an xls file from a spreadsheet builder
+ def issues_to_xls(options)
+ build_spreadsheet(@project, @issues, @query, options).xls
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/spreadsheet_builder.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/spreadsheet_builder.rb
new file mode 100644
index 0000000000..43d8e1c8f6
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/spreadsheet_builder.rb
@@ -0,0 +1,179 @@
+require 'spreadsheet'
+
+# A simple convenience class that wraps some of the spreadsheet
+# gem's functionality. It's designed to build spreadsheets incrementally
+# by adding row after row, but can be used for random access to the
+# rows as well
+#
+# Multiple Worksheets are possible, the currently active worksheet and it's
+# associated column widths are always accessible through the @sheet and @column_widths
+# instance variables, the other worksheets are accessible through the #worksheet method.
+# If a worksheet with an index larger than the number of worksheets is requested,
+# a new worksheet is created.
+#
+
+module OpenProject::XlsExport
+ class SpreadsheetBuilder
+
+ Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet
+
+ def initialize(name = nil)
+ Spreadsheet.client_encoding = 'UTF-8'
+ @xls = Spreadsheet::Workbook.new
+ @worksheets = []
+ worksheet(0, name)
+ end
+
+ # Retrieve or create the worksheet at index x
+ def worksheet(idx, name = nil)
+ name ||= "Worksheet #{@worksheets.length + 1}"
+ if @worksheets[idx].nil?
+ @worksheets[idx] = Worksheet.new.tap do |wb|
+ wb.sheet = @xls.create_worksheet(:name => name)
+ wb.sheet.default_format.vertical_align = :top
+ wb.column_widths = []
+ end
+ end
+
+ @sheet = @worksheets[idx].sheet
+ @column_widths = @worksheets[idx].column_widths
+ end
+
+ # Update column widths and wrap text if neccessary
+ def update_sheet_widths
+ @column_widths.count.times do |idx|
+ if @column_widths[idx] > 60
+ @sheet.column(idx).width = 60
+ @sheet.rows.each do |row|
+ fmt = row.formats[idx] || @sheet.column(idx).default_format
+ fmt.text_wrap = true
+ row.set_format(idx, fmt)
+ end
+ else
+ @sheet.column(idx).width = @column_widths[idx]
+ end
+ end
+ end
+
+ # Get the approximate width of a value as seen in the excel sheet
+ def get_value_width(value)
+ if ['Time', 'Date'].include?(value.class.name)
+ return 18 unless value.to_s.length < 18
+ end
+
+ tot_w = [Float(0)]
+ idx=0
+ value.to_s.each_char do |c|
+ case c
+ when '0'..'9'
+ tot_w[idx] += 1.2
+ when '.', ';', ':', ',', ' ', 'i', 'I', 'j', 'J', '(', ')', '[', ']', '!', '-', 't', 'l'
+ tot_w[idx] += 0.7
+ when 'W', 'M', 'D'
+ tot_w[idx] += 1.2
+ when "\n"
+ idx = idx + 1
+ tot_w << Float(0)
+ else
+ tot_w[idx] += 1.05
+ end
+ end
+
+ wdth=0
+ tot_w.each do |w|
+ wdth = w unless w :bold, :size => 18)
+ @sheet.row(0).set_format(0, title_format)
+ end
+
+ # Add an empty row in the next sequential position. Convenience method
+ # for calling add_row([""])
+ def add_empty_row
+ add_row([""])
+ end
+
+ # Add headers. This is usually used for adding a table header to the
+ # second row in the document, but the row can be set using the second
+ # optional parameter. The format is automatically set to bold font
+ def add_headers(arr, idx = nil)
+ header_format = Spreadsheet::Format.new(:weight => :bold)
+ add_row(arr, idx)
+ idx ||= @sheet.last_row_index
+ (arr.size + 1).times { |i| @sheet.row(idx).set_format(i, header_format) }
+ end
+
+ # Add a simple row. This will default to the next row in the sequence.
+ # Fixnums, Dates and Times are preserved, all other types are converted
+ # to String as the spreadsheet gem cannot do more formats
+ def add_row(arr, idx = nil)
+ idx ||= [@sheet.last_row_index + 1, 1].max
+ column_array = []
+ arr.each_with_index do |c,i|
+ value = if ['Time', 'Date', 'Fixnum', 'Float', 'Integer'].include?(c.class.name)
+ c
+ elsif c.class == BigDecimal
+ c.to_f
+ else
+ c.to_s.gsub('_', ' ').gsub("\r\n", "\n").gsub("\r", "\n")
+ end
+ column_array << value
+ @column_widths[i] = 0 if @column_widths[i].nil?
+ value_width = get_value_width(value)
+ @column_widths[i] = value_width if @column_widths[i] < value_width
+ end
+ @sheet.row(idx).concat column_array
+ end
+
+ # Add a default format to the column at index
+ def add_format_option_to_column(index, opt)
+ unless opt.empty?
+ fmt = @sheet.column(index).default_format.clone
+ opt.each do |k,v|
+ fmt.send(:"#{k.to_sym}=", v) if fmt.respond_to? :"#{k.to_sym}="
+ end
+ @sheet.column(index).default_format = fmt
+ end
+ end
+
+ # Return the next free row we would write to in natural indexing (Starting at 1)
+ def current_row
+ @sheet.row_count
+ end
+
+ # Return the xls file as a string
+ def xls
+ @worksheets.length.times do |i|
+ worksheet(i)
+ update_sheet_widths
+ end
+ io = StringIO.new
+ @xls.write(io)
+ io.rewind
+ io.read
+ end
+
+ private
+ def raw_xls
+ @xls
+ end
+
+ def raw_sheet
+ @sheet
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/version.rb b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/version.rb
new file mode 100644
index 0000000000..3c89808676
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/open_project/xls_export/version.rb
@@ -0,0 +1,5 @@
+module OpenProject
+ module XlsExport
+ VERSION = "5.0.10"
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/lib/openproject-xls_export.rb b/vendored-plugins/openproject-xls_export/lib/openproject-xls_export.rb
new file mode 100644
index 0000000000..912133ee04
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/lib/openproject-xls_export.rb
@@ -0,0 +1 @@
+require 'open_project/xls_export'
diff --git a/vendored-plugins/openproject-xls_export/openproject-xls_export.gemspec b/vendored-plugins/openproject-xls_export/openproject-xls_export.gemspec
new file mode 100644
index 0000000000..1f2c6867f9
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/openproject-xls_export.gemspec
@@ -0,0 +1,21 @@
+# encoding: UTF-8
+$:.push File.expand_path("../lib", __FILE__)
+
+require 'open_project/xls_export/version'
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "openproject-xls_export"
+ s.version = OpenProject::XlsExport::VERSION
+ s.authors = "OpenProject GmbH"
+ s.email = "info@openproject.com"
+ s.homepage = "https://community.openproject.org/projects/export"
+ s.summary = 'OpenProject XLS Export'
+ s.description = 'Export issue lists as Excel spreadsheets (.xls). Support for exporting
+ cost entries and cost reports is not yet migrated to Rails 3 and disabled.'
+ s.license = "GPLv3"
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + %w(CHANGELOG.md README.md)
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency "spreadsheet", "~>0.8.9"
+end
diff --git a/vendored-plugins/openproject-xls_export/package.json b/vendored-plugins/openproject-xls_export/package.json
new file mode 100644
index 0000000000..dc245cfb84
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "openproject-xls_export",
+ "version": "4.1.0",
+ "main": "frontend/app/openproject-xls_export-app.js",
+ "dependencies": {}
+}
diff --git a/vendored-plugins/openproject-xls_export/spec/lib/spreadsheet_builder_spec.rb b/vendored-plugins/openproject-xls_export/spec/lib/spreadsheet_builder_spec.rb
new file mode 100644
index 0000000000..be269bcc39
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/spec/lib/spreadsheet_builder_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe "SpreadsheetBuilder" do
+ before(:each) do
+ @spreadsheet = OpenProject::XlsExport::SpreadsheetBuilder.new
+ @sheet = @spreadsheet.send(:raw_sheet)
+ end
+
+ it "should add a single title in the first row" do
+ @spreadsheet.add_title("A fancy title")
+ expect(@sheet.last_row_index).to eq(0)
+ end
+
+ it "should add the title complety in the first cell" do
+ title = "A fancy title"
+ @spreadsheet.add_title(title)
+ expect(@sheet.last_row[0]).to eq(title)
+ expect(@sheet.last_row[1]).to eq(nil)
+ end
+
+ it "should overwrite titles in consecutive calls" do
+ title = "A fancy title"
+ @spreadsheet.add_title(title)
+ @spreadsheet.add_title(title)
+ expect(@sheet.last_row_index).to eq(0)
+ end
+
+ it "should do some formatting on the title" do
+ @spreadsheet.add_title("A fancy title")
+ expect(@sheet.last_row.format(0)).not_to eq(@sheet.last_row.format(1))
+ end
+
+ it "should add empty rows starting in the second line" do
+ @spreadsheet.add_empty_row
+ expect(@sheet.last_row_index).to eq(1)
+ end
+
+ it "should add empty rows at the next sequential row" do
+ @spreadsheet.add_empty_row
+ first = @sheet.last_row_index
+ @spreadsheet.add_empty_row
+ expect(@sheet.last_row_index).to eq(first + 1)
+ end
+
+ it "should add headers in the second line per default" do
+ @spreadsheet.add_headers((1..3).to_a)
+ expect(@sheet.last_row_index).to eq(1)
+ end
+
+ it "should allow adding headers in the first line" do
+ @spreadsheet.add_headers((1..3).to_a, 0)
+ expect(@sheet.last_row_index).to eq(0)
+ end
+
+ it "should add headers with some formatting" do
+ @spreadsheet.add_headers([1], 0)
+ expect(@sheet.last_row.format(0)).not_to eq(@sheet.last_row.format(2))
+ end
+
+ it "should start adding rows in the first line" do
+ @spreadsheet.add_row((1..3).to_a)
+ expect(@sheet.last_row_index).to eq(1)
+ end
+
+ it "should add rows sequentially" do
+ @spreadsheet.add_row((1..3).to_a)
+ first = @sheet.last_row_index
+ @spreadsheet.add_row((1..3).to_a)
+ expect(@sheet.last_row_index).to eq(first + 1)
+ end
+
+ it "should apply no formatting on rows" do
+ @spreadsheet.add_row([1])
+ expect(@sheet.last_row.format(0)).to eq(@sheet.last_row.format(1))
+ end
+
+ it "should always use unix newlines" do
+ @spreadsheet.add_row(["Some text including a windows newline (\r\n)", "And an old-style mac os newline (\r)"])
+ 2.times do |i|
+ expect(@spreadsheet.send("raw_sheet").last_row[i]).not_to include("\r")
+ expect(@spreadsheet.send("raw_sheet").last_row[i]).not_to include("\r\n")
+ expect(@spreadsheet.send("raw_sheet").last_row[i]).to include("\n")
+ end
+ end
+end
diff --git a/vendored-plugins/openproject-xls_export/spec/patches/cost_reports_controller_patch_spec.rb b/vendored-plugins/openproject-xls_export/spec/patches/cost_reports_controller_patch_spec.rb
new file mode 100644
index 0000000000..ead956322b
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/spec/patches/cost_reports_controller_patch_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'CostReportsController', "rendering to xls" do
+ skip 'XlsExport: CostReports support not yet migrated to Rails 3'
+
+ it "should respond with the xls if requested in the index" do
+ skip
+ render :action => :index
+ expect(response).to be_redirect
+ end
+
+ it "should not respond with the xls if requested in a detail view" do
+ skip
+ render :action => :show
+ expect(response).to be_redirect
+ end
+
+ it "should generate xls from issues" do
+ skip
+ end
+
+end
diff --git a/vendored-plugins/openproject-xls_export/spec/patches/work_packages_controller_patch_spec.rb b/vendored-plugins/openproject-xls_export/spec/patches/work_packages_controller_patch_spec.rb
new file mode 100644
index 0000000000..5a66f83f1b
--- /dev/null
+++ b/vendored-plugins/openproject-xls_export/spec/patches/work_packages_controller_patch_spec.rb
@@ -0,0 +1,198 @@
+require 'spec_helper'
+
+describe WorkPackagesController, "rendering to xls", :type => :controller do
+ let(:current_user) { FactoryGirl.create(:admin) }
+ let!(:work_package) { FactoryGirl.create(:work_package, :subject => '!SUBJECT!',
+ :description => '!DESCRIPTION!') }
+
+ before do
+ allow(User).to receive(:current).and_return current_user
+ end
+
+ describe "should respond with the xls if requested in the index" do
+ before do
+ get('index', :format => 'xls', :project_id => work_package.project_id)
+ end
+
+ it 'should respond with 200 OK' do
+ expect(response.response_code).to eq(200)
+ end
+
+ it 'should have a length > 100 bytes' do
+ expect(response.body.length).to be > 100
+ end
+
+ it 'should not contain a description' do
+ expect(response.body).not_to include('!DESCRIPTION!')
+ end
+
+ it 'should contain a subject' do
+ expect(response.body).to include('!SUBJECT!')
+ end
+
+ context 'the mime type' do
+ it { expect(response.header['Content-Type']).to eq('application/vnd.ms-excel') }
+ end
+ end
+
+ describe 'with cost and time entries' do
+ # Since this test has to work without the actual costs plugin we'll just add
+ # a custom field called 'costs' to emulate it.
+
+ let(:custom_field) { FactoryGirl.create(:work_package_custom_field, :name => 'unit costs', :field_format => 'float') }
+ let(:custom_value) { FactoryGirl.create(:custom_value, :custom_field => custom_field) }
+ let(:project) { FactoryGirl.create(:project, :work_package_custom_fields => [custom_field]) }
+ let(:work_packages) do
+ value = lambda do |val|
+ FactoryGirl.create(:custom_value, :custom_field => custom_field, :value => val)
+ end
+ wps = FactoryGirl.create_list(:work_package, 4, :project => project)
+ wps[0].estimated_hours = 27.5
+ wps[0].save!
+ wps[1].custom_values << value.call(1)
+ wps[2].custom_values << value.call(99.99)
+ wps[3].custom_values << value.call(1000)
+ wps
+ end
+
+ before do
+ allow(OpenProject::XlsExport::Formatters::TimeFormatter).to receive(:apply?) do |column|
+ column.caption =~ /time/i
+ end
+
+ allow(OpenProject::XlsExport::Formatters::CostFormatter).to receive(:apply?) do |column|
+ column.caption =~ /cost/i
+ end
+
+ allow(Setting).to receive(:plugin_openproject_costs).and_return({ 'costs_currency' => 'EUR','costs_currency_format' => '%n %u' })
+
+ get 'index',
+ :format => 'xls',
+ :project_id => work_packages.first.project_id,
+ :set_filter => '1',
+ :c => ['subject', 'status', 'estimated_hours', "cf_#{custom_field.id}"]
+
+ expect(response.response_code).to eq(200)
+
+ f = Tempfile.new 'result.xls'
+ begin
+ f.binmode
+ f.write response.body
+ ensure
+ f.close
+ end
+
+ require 'spreadsheet'
+
+ @sheet = Spreadsheet.open(f.path).worksheets.first
+ f.unlink
+ end
+
+ it 'should successfully export the work packages with a cost column' do
+ expect(@sheet.rows.size).to eq(4 + 1)
+
+ cost_column = @sheet.columns.last.to_a
+ [1, 99.99, 1000].each do |value|
+ expect(cost_column).to include(value)
+ end
+ end
+
+ it 'should include estimated hours' do
+ expect(@sheet.rows.size).to eq(4 + 1)
+
+ hours = @sheet.rows.last.values_at(2)
+ expect(hours).to include(27.5)
+ end
+ end
+
+ context 'with descriptions' do
+ before do
+ get('index', :format => 'xls',
+ :project_id => work_package.project_id,
+ :show_descriptions => 'true')
+ end
+
+ it 'should respond with 200 OK' do
+ expect(response.response_code).to eq(200)
+ end
+
+ it 'should have a length > 100 bytes' do
+ expect(response.body.length).to be > 100
+ end
+
+ it 'should contain a description' do
+ expect(response.body).to include('!DESCRIPTION!')
+ end
+
+ it 'should contain a subject' do
+ expect(response.body).to include('!SUBJECT!')
+ end
+
+ context 'the mime type' do
+ it { expect(response.header['Content-Type']).to eq('application/vnd.ms-excel') }
+ end
+ end
+
+ describe 'empty result' do
+ before do
+ work_package.delete
+
+ get 'index', :format => 'xls', :project_id => work_package.project_id
+ end
+
+ it 'should yield an empty XLS file' do
+ expect(response.response_code).to be(200)
+
+ f = Tempfile.new 'result.xls'
+ begin
+ f.binmode
+ f.write response.body
+ ensure
+ f.close
+ end
+
+ require 'spreadsheet'
+
+ sheet = Spreadsheet.open(f.path).worksheets.first
+ expect(sheet.rows.size).to eq(1) # just the headers
+ end
+ end
+
+ describe 'with user time zone' do
+ let(:zone) { +2 }
+
+ before do
+ allow(current_user).to receive(:time_zone).and_return(zone)
+
+ allow(OpenProject::XlsExport::Formatters::TimeFormatter).to receive(:apply?) do |column|
+ column.caption =~ /time/i
+ end
+
+ get 'index',
+ :format => 'xls',
+ :project_id => work_package.project_id,
+ :set_filter => '1',
+ :c => ['subject', 'status', 'updated_at']
+
+ expect(response.response_code).to eq(200)
+
+ f = Tempfile.new 'result.xls'
+ begin
+ f.binmode
+ f.write response.body
+ ensure
+ f.close
+ end
+
+ require 'spreadsheet'
+
+ @sheet = Spreadsheet.open(f.path).worksheets.first
+ f.unlink
+ end
+
+ it 'should adapt the datetime fields to the user time zone' do
+ updated_at_cell = @sheet.rows.last.to_a.last
+ expect(updated_at_cell.to_s(:number)).to eq(work_package.updated_at.in_time_zone(zone).to_s(:number))
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/.hound.yml b/vendored-plugins/reporting_engine/.hound.yml
new file mode 100644
index 0000000000..b7945ab908
--- /dev/null
+++ b/vendored-plugins/reporting_engine/.hound.yml
@@ -0,0 +1,3 @@
+ruby:
+ enabled: true
+ config_file: .rubocop.yml
diff --git a/vendored-plugins/reporting_engine/.rubocop.yml b/vendored-plugins/reporting_engine/.rubocop.yml
new file mode 100644
index 0000000000..c3f9ba14a3
--- /dev/null
+++ b/vendored-plugins/reporting_engine/.rubocop.yml
@@ -0,0 +1,265 @@
+AllCops:
+ Exclude:
+ - "*.gemspec"
+
+AccessorMethodName:
+ Enabled: false
+
+ActionFilter:
+ Enabled: false
+
+Alias:
+ Enabled: false
+
+AndOr:
+ Enabled: false
+
+ArrayJoin:
+ Enabled: false
+
+AsciiComments:
+ Enabled: false
+
+AsciiIdentifiers:
+ Enabled: false
+
+Attr:
+ Enabled: false
+
+BlockNesting:
+ Enabled: false
+
+Blocks:
+ Enabled: false
+
+CaseEquality:
+ Enabled: false
+
+CharacterLiteral:
+ Enabled: false
+
+ClassAndModuleChildren:
+ Enabled: false
+
+ClassLength:
+ Enabled: false
+
+ClassVars:
+ Enabled: false
+
+CollectionMethods:
+ PreferredMethods:
+ find: detect
+ reduce: inject
+ collect: map
+ find_all: select
+
+ColonMethodCall:
+ Enabled: false
+
+CommentAnnotation:
+ Enabled: false
+
+CyclomaticComplexity:
+ Enabled: false
+
+Delegate:
+ Enabled: false
+
+DeprecatedHashMethods:
+ Enabled: false
+
+Documentation:
+ Enabled: false
+
+DotPosition:
+ EnforcedStyle: leading
+
+DoubleNegation:
+ Enabled: false
+
+EachWithObject:
+ Enabled: false
+
+EmptyLiteral:
+ Enabled: false
+
+Encoding:
+ Enabled: false
+
+EvenOdd:
+ Enabled: false
+
+FileName:
+ Enabled: false
+
+FlipFlop:
+ Enabled: false
+
+FormatString:
+ Enabled: false
+
+GlobalVars:
+ Enabled: false
+
+GuardClause:
+ Enabled: false
+
+IfUnlessModifier:
+ Enabled: false
+
+IfWithSemicolon:
+ Enabled: false
+
+InlineComment:
+ Enabled: false
+
+Lambda:
+ Enabled: false
+
+LambdaCall:
+ Enabled: false
+
+LineEndConcatenation:
+ Enabled: false
+
+LineLength:
+ Max: 99
+
+MethodLength:
+ Enabled: false
+
+ModuleFunction:
+ Enabled: false
+
+NegatedIf:
+ Enabled: false
+
+NegatedWhile:
+ Enabled: false
+
+Next:
+ Enabled: false
+
+NilComparison:
+ Enabled: false
+
+Not:
+ Enabled: false
+
+NumericLiterals:
+ Enabled: false
+
+OneLineConditional:
+ Enabled: false
+
+OpMethod:
+ Enabled: false
+
+ParameterLists:
+ Enabled: false
+
+PercentLiteralDelimiters:
+ Enabled: false
+
+PerlBackrefs:
+ Enabled: false
+
+PredicateName:
+ NamePrefixBlacklist:
+ - is_
+
+Proc:
+ Enabled: false
+
+RaiseArgs:
+ Enabled: false
+
+RegexpLiteral:
+ Enabled: false
+
+SelfAssignment:
+ Enabled: false
+
+SingleLineBlockParams:
+ Enabled: false
+
+SingleLineMethods:
+ Enabled: false
+
+SignalException:
+ Enabled: false
+
+SpecialGlobalVars:
+ Enabled: false
+
+StringLiterals:
+ EnforcedStyle: single_quotes
+
+VariableInterpolation:
+ Enabled: false
+
+TrailingComma:
+ Enabled: false
+
+TrivialAccessors:
+ Enabled: false
+
+VariableInterpolation:
+ Enabled: false
+
+WhenThen:
+ Enabled: false
+
+WhileUntilModifier:
+ Enabled: false
+
+WordArray:
+ Enabled: false
+
+# Lint
+
+AmbiguousOperator:
+ Enabled: false
+
+AmbiguousRegexpLiteral:
+ Enabled: false
+
+AssignmentInCondition:
+ Enabled: false
+
+ConditionPosition:
+ Enabled: false
+
+DeprecatedClassMethods:
+ Enabled: false
+
+ElseLayout:
+ Enabled: false
+
+HandleExceptions:
+ Enabled: false
+
+InvalidCharacterLiteral:
+ Enabled: false
+
+LiteralInCondition:
+ Enabled: false
+
+LiteralInInterpolation:
+ Enabled: false
+
+Loop:
+ Enabled: false
+
+ParenthesesAsGroupedExpression:
+ Enabled: false
+
+RequireParentheses:
+ Enabled: false
+
+UnderscorePrefixedVariableName:
+ Enabled: false
+
+Void:
+ Enabled: false
diff --git a/vendored-plugins/reporting_engine/README.md b/vendored-plugins/reporting_engine/README.md
new file mode 100644
index 0000000000..91322a84c5
--- /dev/null
+++ b/vendored-plugins/reporting_engine/README.md
@@ -0,0 +1,66 @@
+ReportingEngine
+===============
+
+The ReportingEngine is a Rails engine containing base functionality to create customized database reports. A report consists of filters and grouping criteria, each of which selects an attribute to be used for filtering and grouping. It provides base filter and grouping classes to be used for adding new filters. It also adds some base widgets to visually represent the created reports.
+
+This engine is mainly used in the [OpenProject Reporting plugin](https://www.openproject.org/projects/plugin-reporting), allowing to create customized cost reports when the [OpenProject Costs plugin](https://www.openproject.org/projects/costs-plugin) is used to track projects costs.
+
+Requirements
+------------
+
+The ReportingEngine requires Rails 3.2 and is compatible with MySQL or PostgreSQL. MySQL versions 5.6.0 - 5.6.12 and 5.7.0 - 5.7.1 are not supported since they contain a bug leading to wrong report results under certain circumstances.
+
+Installation
+------------
+
+To use the ReportingEngine, add the following line to your `Gemfile`:
+
+`gem "reporting_engine", git: "https://github.com/finnlabs/reporting_engine.git", :branch => "dev"`
+
+If you are running OpenProject, add the above line to the `Gemfile.plugins` in your OpenProject installation folder instead.
+
+Afterwards, run:
+
+`bundle install`
+
+
+Deinstallation
+--------------
+
+Remove the line
+
+`gem "reporting_engine", git: "https://github.com/finnlabs/reporting_engine.git", :branch => "dev"`
+
+from your `Gemfile` or the `Gemfile.plugins` in your OpenProject installation and run:
+
+`bundle install`
+
+
+Bug Reporting
+-------------
+
+If you find any bugs, you can create a bug ticket at
+
+https://www.openproject.org/projects/plugin-reportingengine
+
+
+Development
+-----------
+
+To contribute, you can create pull request on the official repository at
+`https://github.com/finnlabs/reporting_engine`
+
+
+Credits
+-------
+
+Special thanks go to
+
+* Deutsche Telekom AG (opensource@telekom.de) for project sponsorship
+
+Licence
+-------
+
+Copyright (C) 2010 - 2015 OpenProject Foundation (OPF)
+
+This plugin is licensed under the GNU GPL v3. See doc/COPYRIGHT.md and doc/GPL.txt for details.
diff --git a/vendored-plugins/reporting_engine/config/locales/da.yml b/vendored-plugins/reporting_engine/config/locales/da.yml
new file mode 100644
index 0000000000..032cab62a4
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/da.yml
@@ -0,0 +1,27 @@
+da:
+ description_drill_down: Vis detaljer
+ label_click_to_edit: Klik for at redigere.
+ label_columns: Kolonner
+ label_count: Antal
+ label_filter: Filter
+ label_filter_add: Tilføj filter
+ label_filter_plural: Filtre
+ label_greater: '>'
+ label_group_by: Gruppér efter
+ label_group_by_add: Tilføj Gruppér efter-attribut
+ label_help: Hjælp
+ label_inactive: «inaktiv»
+ label_less: '<'
+ label_no: Nej
+ label_none: (ingen data)
+ label_progress_bar_explanation: Genererer rapport...
+ label_really_delete_question: Sikker på, at du ønsker denne rapport slettet?
+ label_report: Rapport
+ label_rows: Rækker
+ label_saving: Gemmer ...
+ label_sum: Sum
+ label_yes: Ja
+ load_query_question: 'Rapporten vil indeholde %{size} celler og kan tage nogen tid at danne. Fortsæt alligevel?'
+ units: Enheder
+ validation_failure_date: er en ugyldig dato
+ validation_failure_integer: er et ugyldigt heltal
diff --git a/vendored-plugins/reporting_engine/config/locales/de.yml b/vendored-plugins/reporting_engine/config/locales/de.yml
new file mode 100644
index 0000000000..a917663a65
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/de.yml
@@ -0,0 +1,27 @@
+de:
+ description_drill_down: Details anzeigen
+ label_click_to_edit: Zum Bearbeiten hier klicken.
+ label_columns: Spalten
+ label_count: Anzahl
+ label_filter: Filter
+ label_filter_add: Filter hinzufügen
+ label_filter_plural: Filter
+ label_greater: '>'
+ label_group_by: Gruppieren nach
+ label_group_by_add: Gruppierung hinzufügen
+ label_help: Hilfe
+ label_inactive: «nicht aktiviert»
+ label_less: '<'
+ label_no: Nein
+ label_none: (Keine Angabe)
+ label_progress_bar_explanation: Report wird erstellt ...
+ label_really_delete_question: Diesen Report wirklich löschen?
+ label_report: Report
+ label_rows: Zeilen
+ label_saving: Speichern ...
+ label_sum: Summe
+ label_yes: Ja
+ load_query_question: 'Der Report wird %{size} Tabellen-Zellen haben, was sehr rechenintensiv sein kann. Wollen Sie dennoch versuchen, den Report durch zu führen?'
+ units: Einheiten
+ validation_failure_date: ist kein gültiges Datum
+ validation_failure_integer: ist keine ganze Zahl
diff --git a/vendored-plugins/reporting_engine/config/locales/en.yml b/vendored-plugins/reporting_engine/config/locales/en.yml
new file mode 100644
index 0000000000..4882778801
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/en.yml
@@ -0,0 +1,50 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+---
+en:
+ description_drill_down: "Show details"
+
+ label_click_to_edit: "Click to edit."
+ label_columns: "Columns"
+ label_count: "Count"
+ label_filter: "Filter"
+ label_filter_add: "Add Filter"
+ label_filter_plural: "Filters"
+ label_greater: ">"
+ label_group_by: "Group by"
+ label_group_by_add: "Add Group-by Attribute"
+ label_help: "Help"
+ label_inactive: "«inactive»"
+ label_less: "<"
+ label_no: "No"
+ label_none: "(no data)"
+ label_progress_bar_explanation: "Generating report..."
+ label_really_delete_question: "Are you sure you want to delete this report?"
+ label_report: "Report"
+ label_rows: "Rows"
+ label_saving: "Saving ..."
+ label_sum: "Sum"
+ label_yes: "Yes"
+ load_query_question: "Report will have %{size} table cells and may take some time to render. Do you still want to try rendering it?"
+
+ units: "Units"
+
+ validation_failure_date: "is not a valid date"
+ validation_failure_integer: "is not a valid integer"
diff --git a/vendored-plugins/reporting_engine/config/locales/et.yml b/vendored-plugins/reporting_engine/config/locales/et.yml
new file mode 100644
index 0000000000..5646403c77
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/et.yml
@@ -0,0 +1,27 @@
+et:
+ description_drill_down: Näita üksikasju
+ label_click_to_edit: Muutmiseks kliki.
+ label_columns: Veerud
+ label_count: Arv
+ label_filter: Filter
+ label_filter_add: Lisa filter
+ label_filter_plural: Filtrid
+ label_greater: '>'
+ label_group_by: Grupeeri
+ label_group_by_add: Lisa grupeerimise omadus
+ label_help: Abi
+ label_inactive: «mitteaktiivne»
+ label_less: '<'
+ label_no: Ei
+ label_none: (andmeid pole)
+ label_progress_bar_explanation: Aruande loomine...
+ label_really_delete_question: Oled sa kindel, et soovid seda aruannet kustutada?
+ label_report: Aruanne
+ label_rows: Read
+ label_saving: Salvestamine ...
+ label_sum: Summa
+ label_yes: Jah
+ load_query_question: 'Aruandes on %{size} tabeli lahtrit ja selle loomine võib veidi aega võtta. Kas sa siiski soovid seda luua?'
+ units: Ühikud
+ validation_failure_date: pole korrektne kuupäev
+ validation_failure_integer: pole täisarv
diff --git a/vendored-plugins/reporting_engine/config/locales/fr.yml b/vendored-plugins/reporting_engine/config/locales/fr.yml
new file mode 100644
index 0000000000..129ffbe6c0
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/fr.yml
@@ -0,0 +1,28 @@
+fr:
+ description_drill_down: Afficher les détails
+ label_click_to_edit: Cliquez pour modifier.
+ label_columns: Colonnes
+ label_count: Décompte
+ label_filter: Filtre
+ label_filter_add: Ajouter un filtre
+ label_filter_plural: Filtres
+ label_greater: '>'
+ label_group_by: Grouper par
+ label_group_by_add: |-
+ Ajoutez l'attribut "Grouper par"
+ label_help: Aide
+ label_inactive: « inactif »
+ label_less: '<'
+ label_no: Non
+ label_none: (aucune donnée)
+ label_progress_bar_explanation: Rapport en cours de génération...
+ label_really_delete_question: Êtes-vous sûr de vouloir supprimer ce rapport ?
+ label_report: Rapport
+ label_rows: Lignes
+ label_saving: Enregistrement en cours …
+ label_sum: Total
+ label_yes: Oui
+ load_query_question: 'Le rapport contiendra un tableau de %{size} cellules et peut prendre un certain temps à être généré. Voulez-vous quand même le générer ?'
+ units: Unités
+ validation_failure_date: "n'est pas une date valide"
+ validation_failure_integer: "n'est pas un nombre entier valide"
diff --git a/vendored-plugins/reporting_engine/config/locales/hr.yml b/vendored-plugins/reporting_engine/config/locales/hr.yml
new file mode 100644
index 0000000000..12c16546eb
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/hr.yml
@@ -0,0 +1,27 @@
+hr:
+ description_drill_down: Prikaži detalje
+ label_click_to_edit: Kliknite za uređivanje.
+ label_columns: Stupci
+ label_count: Zbroji
+ label_filter: Filter
+ label_filter_add: Dodaj filter
+ label_filter_plural: Filteri
+ label_greater: '>'
+ label_group_by: Grupiraj po
+ label_group_by_add: 'Dodaj grupu - po Atributu'
+ label_help: Pomoć
+ label_inactive: «neaktivan»
+ label_less: '<'
+ label_no: Ne
+ label_none: (nema podataka)
+ label_progress_bar_explanation: Generiranje izvješća...
+ label_really_delete_question: Da li ste sigurni da želite izbrisati ovo izvješće?
+ label_report: Izvješće
+ label_rows: Redci
+ label_saving: Spremanje ...
+ label_sum: Zbroj
+ label_yes: Da
+ load_query_question: 'Izvješće će imati %{size} veličinu tablice te će biti potrebno određeno vrijeme za prikaz. Da li želite prikazati ga i dalje?'
+ units: Jedinice
+ validation_failure_date: nije valjan datum
+ validation_failure_integer: nije valjan cijeli broj
diff --git a/vendored-plugins/reporting_engine/config/locales/it.yml b/vendored-plugins/reporting_engine/config/locales/it.yml
new file mode 100644
index 0000000000..26dd419e4a
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/it.yml
@@ -0,0 +1,27 @@
+it:
+ description_drill_down: Mostra dettagli
+ label_click_to_edit: Premi per modificare.
+ label_columns: Colonne
+ label_count: Conteggio
+ label_filter: Filtro
+ label_filter_add: Aggiungi filtro
+ label_filter_plural: Filtri
+ label_greater: '>'
+ label_group_by: Raggruppa per
+ label_group_by_add: "Aggiungi l'attributo Group-by"
+ label_help: Aiuto
+ label_inactive: «inattivo»
+ label_less: '<'
+ label_no: 'No'
+ label_none: (nessun dato)
+ label_progress_bar_explanation: Generazione Report...
+ label_really_delete_question: Sei sicuro di voler cancellare questo report?
+ label_report: Report
+ label_rows: Righe
+ label_saving: Salvataggio in corso ...
+ label_sum: Somma
+ label_yes: Si
+ load_query_question: 'Il report avrà le celle della tabella del %{size} e potrebbe essere necessario del tempo per eseguire il rendering. Vuoi ancora tentare di eseguirne il rendering?'
+ units: Unità di misura
+ validation_failure_date: non è una data valida
+ validation_failure_integer: non è un numero intero valido
diff --git a/vendored-plugins/reporting_engine/config/locales/ja.yml b/vendored-plugins/reporting_engine/config/locales/ja.yml
new file mode 100644
index 0000000000..1d6fe013a9
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/ja.yml
@@ -0,0 +1,27 @@
+ja:
+ description_drill_down: 詳細を表示
+ label_click_to_edit: クリックして編集
+ label_columns: 列
+ label_count: 回数
+ label_filter: フィルタ
+ label_filter_add: フィルタを追加
+ label_filter_plural: フィルタ
+ label_greater: '>'
+ label_group_by: グループ化
+ label_group_by_add: グループ化属性を追加
+ label_help: ヘルプ
+ label_inactive: «非活動»
+ label_less: '<'
+ label_no: いいえ
+ label_none: (データなし)
+ label_progress_bar_explanation: レポートを生成中...
+ label_really_delete_question: このレポートを削除してもよろしいですか?
+ label_report: レポート
+ label_rows: 行
+ label_saving: 保存中…
+ label_sum: 合計
+ label_yes: はい
+ load_query_question: 'レポートは%{size}つセルを持つために、描画するに時間がかかる場合があります。描画してもよろしいですか?'
+ units: 単位
+ validation_failure_date: は有効な日付ではありません。
+ validation_failure_integer: は有効な整数ではありません。
diff --git a/vendored-plugins/reporting_engine/config/locales/js-da.yml b/vendored-plugins/reporting_engine/config/locales/js-da.yml
new file mode 100644
index 0000000000..d9091c3ddd
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-da.yml
@@ -0,0 +1,5 @@
+da:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-de.yml b/vendored-plugins/reporting_engine/config/locales/js-de.yml
new file mode 100644
index 0000000000..181166f147
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-de.yml
@@ -0,0 +1,5 @@
+de:
+ js:
+ reporting_engine:
+ label_remove: Löschen
+ label_response_error: Bei der Bearbeitung der Anfrage ist ein Fehler aufgetreten.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-en.yml b/vendored-plugins/reporting_engine/config/locales/js-en.yml
new file mode 100644
index 0000000000..f4963ee0e7
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-en.yml
@@ -0,0 +1,23 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+en:
+ js:
+ reporting_engine:
+ label_remove: "Delete"
+ label_response_error: "There was an error handling the query."
diff --git a/vendored-plugins/reporting_engine/config/locales/js-et.yml b/vendored-plugins/reporting_engine/config/locales/js-et.yml
new file mode 100644
index 0000000000..42ca30ec53
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-et.yml
@@ -0,0 +1,5 @@
+et:
+ js:
+ reporting_engine:
+ label_remove: Kustuta
+ label_response_error: Päringu käsitlemisel tekkis tõrge.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-fr.yml b/vendored-plugins/reporting_engine/config/locales/js-fr.yml
new file mode 100644
index 0000000000..dfc85fb9ad
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-fr.yml
@@ -0,0 +1,5 @@
+fr:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-hr.yml b/vendored-plugins/reporting_engine/config/locales/js-hr.yml
new file mode 100644
index 0000000000..db98065fa4
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-hr.yml
@@ -0,0 +1,5 @@
+hr:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-it.yml b/vendored-plugins/reporting_engine/config/locales/js-it.yml
new file mode 100644
index 0000000000..7875ab6f1e
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-it.yml
@@ -0,0 +1,5 @@
+it:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-ja.yml b/vendored-plugins/reporting_engine/config/locales/js-ja.yml
new file mode 100644
index 0000000000..0cffddbecd
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-ja.yml
@@ -0,0 +1,5 @@
+ja:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-pt-BR.yml b/vendored-plugins/reporting_engine/config/locales/js-pt-BR.yml
new file mode 100644
index 0000000000..8ac3abf886
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-pt-BR.yml
@@ -0,0 +1,5 @@
+pt-BR:
+ js:
+ reporting_engine:
+ label_remove: Excluir
+ label_response_error: Ocorreu um erro na manipulação da consulta.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-ru.yml b/vendored-plugins/reporting_engine/config/locales/js-ru.yml
new file mode 100644
index 0000000000..0e022edf35
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-ru.yml
@@ -0,0 +1,5 @@
+ru:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-sk.yml b/vendored-plugins/reporting_engine/config/locales/js-sk.yml
new file mode 100644
index 0000000000..dda7b9da98
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-sk.yml
@@ -0,0 +1,5 @@
+sk:
+ js:
+ reporting_engine:
+ label_remove: Odstrániť
+ label_response_error: Počas spracovania dotazu nastala chyba.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-sv-SE.yml b/vendored-plugins/reporting_engine/config/locales/js-sv-SE.yml
new file mode 100644
index 0000000000..7ff3ded582
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-sv-SE.yml
@@ -0,0 +1,5 @@
+sv:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/js-tr.yml b/vendored-plugins/reporting_engine/config/locales/js-tr.yml
new file mode 100644
index 0000000000..560ba67a77
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/js-tr.yml
@@ -0,0 +1,5 @@
+tr:
+ js:
+ reporting_engine:
+ label_remove: Delete
+ label_response_error: There was an error handling the query.
diff --git a/vendored-plugins/reporting_engine/config/locales/pt-BR.yml b/vendored-plugins/reporting_engine/config/locales/pt-BR.yml
new file mode 100644
index 0000000000..384e56b297
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/pt-BR.yml
@@ -0,0 +1,27 @@
+pt-BR:
+ description_drill_down: Exibir detalhes
+ label_click_to_edit: Clique para editar.
+ label_columns: Colunas
+ label_count: Contar
+ label_filter: Filtro
+ label_filter_add: Adicionar filtro
+ label_filter_plural: Filtros
+ label_greater: '>'
+ label_group_by: Agrupar por
+ label_group_by_add: Adicionar atributo Agrupar por
+ label_help: Ajuda
+ label_inactive: «inativo»
+ label_less: '<'
+ label_no: Não
+ label_none: (sem dados)
+ label_progress_bar_explanation: Gerando relatório...
+ label_really_delete_question: Tem certeza que deseja excluir este relatório?
+ label_report: Relatório
+ label_rows: Linhas
+ label_saving: Salvando ...
+ label_sum: Soma
+ label_yes: Sim
+ load_query_question: 'Relatório terá %{size} células da tabela e pode levar algum tempo para processar. Você ainda quer tentar processá-lo?'
+ units: Unidades
+ validation_failure_date: não é uma data válida
+ validation_failure_integer: não é um valor inteiro válido
diff --git a/vendored-plugins/reporting_engine/config/locales/ru.yml b/vendored-plugins/reporting_engine/config/locales/ru.yml
new file mode 100644
index 0000000000..bcf95669d0
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/ru.yml
@@ -0,0 +1,27 @@
+ru:
+ description_drill_down: Показать в деталях
+ label_click_to_edit: Щелкнуть для изменения.
+ label_columns: Столбцы
+ label_count: Количество
+ label_filter: Фильтр
+ label_filter_add: Добавить фильтр
+ label_filter_plural: Фильтры
+ label_greater: '>'
+ label_group_by: Группировать по
+ label_group_by_add: 'Добавить атрибут "Группировать по"'
+ label_help: Справка
+ label_inactive: «неактивен»
+ label_less: '<'
+ label_no: Нет
+ label_none: (нет данных)
+ label_progress_bar_explanation: Создание отчета...
+ label_really_delete_question: Вы действительно хотите удалить этот отчет?
+ label_report: Отчет
+ label_rows: Строки
+ label_saving: Сохранение ...
+ label_sum: Сумма
+ label_yes: Да
+ load_query_question: 'Отчет будет иметь %{size} ячеек таблицы и может занять некоторое время для визуализации. Вы все еще хотите его сформировать?'
+ units: Модули
+ validation_failure_date: не является допустимой датой
+ validation_failure_integer: не является допустимым целым числом
diff --git a/vendored-plugins/reporting_engine/config/locales/sk.yml b/vendored-plugins/reporting_engine/config/locales/sk.yml
new file mode 100644
index 0000000000..91d6fa4a33
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/sk.yml
@@ -0,0 +1,27 @@
+sk:
+ description_drill_down: Zobraziť Podrobnosti
+ label_click_to_edit: Kliknite pre úpravu.
+ label_columns: Stĺpce
+ label_count: Počet
+ label_filter: Filtrovať
+ label_filter_add: Pridať filter
+ label_filter_plural: Filtre
+ label_greater: '>'
+ label_group_by: Zoskupiť podľa
+ label_group_by_add: 'Pridať atribút "zoskupiť podľa"'
+ label_help: Nápoveda
+ label_inactive: «neaktívny»
+ label_less: '<'
+ label_no: Nie
+ label_none: (žiadne údaje)
+ label_progress_bar_explanation: Prebieha tvorba tlačovej zostavy...
+ label_really_delete_question: Určite chcete odstrániť túto tlačovú zostavu?
+ label_report: Tlačová zostava
+ label_rows: Riadky
+ label_saving: Ukladanie ...
+ label_sum: Súčet
+ label_yes: Áno
+ load_query_question: 'Výsledná tlačová zostava bude obsahovať %{size} buniek a jej vytvorenie môže trvať dlhšie. Želáte si ju aj napriek tomu vygenerovať?'
+ units: Jednotky
+ validation_failure_date: nie je platný dátum
+ validation_failure_integer: nie je platné celé číslo
diff --git a/vendored-plugins/reporting_engine/config/locales/sv-SE.yml b/vendored-plugins/reporting_engine/config/locales/sv-SE.yml
new file mode 100644
index 0000000000..8359cbe5af
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/sv-SE.yml
@@ -0,0 +1,27 @@
+sv:
+ description_drill_down: Visa detaljer
+ label_click_to_edit: Klicka för att ändra.
+ label_columns: Kolumner
+ label_count: Antal
+ label_filter: Filter
+ label_filter_add: Lägg till filter
+ label_filter_plural: Filter
+ label_greater: '>'
+ label_group_by: Gruppera efter
+ label_group_by_add: Lägg till Gruppera-efter-Attribut
+ label_help: Hjälp
+ label_inactive: «inaktiv»
+ label_less: '<'
+ label_no: Nej
+ label_none: (inga data)
+ label_progress_bar_explanation: Genererar rapport...
+ label_really_delete_question: Är du säker på att du vill ta bort denna rapport?
+ label_report: Rapport
+ label_rows: Rader
+ label_saving: Sparar ...
+ label_sum: Summa
+ label_yes: Ja
+ load_query_question: 'Rapporten kommer att ha %{size} celler och kan ta lite tid att skapa. Vill du prova att skapa den ändå?'
+ units: Enheter
+ validation_failure_date: inte är ett giltigt datum
+ validation_failure_integer: är inte ett giltigt heltal
diff --git a/vendored-plugins/reporting_engine/config/locales/tr.yml b/vendored-plugins/reporting_engine/config/locales/tr.yml
new file mode 100644
index 0000000000..0abe954c9f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/config/locales/tr.yml
@@ -0,0 +1,27 @@
+tr:
+ description_drill_down: Ayrıntıları görüntüle
+ label_click_to_edit: Düzenlemek için tıklayınız.
+ label_columns: Sütunlar
+ label_count: Count
+ label_filter: Filter
+ label_filter_add: Süzgeç ekle
+ label_filter_plural: Süzgeçler
+ label_greater: '>'
+ label_group_by: Grupla
+ label_group_by_add: Add Group-by Attribute
+ label_help: Yardım
+ label_inactive: «inactive»
+ label_less: '<'
+ label_no: Hayır
+ label_none: (veri yok)
+ label_progress_bar_explanation: Rapor oluşturuluyor...
+ label_really_delete_question: Bu raporu silmek istediğinizden emin misiniz?
+ label_report: Rapor
+ label_rows: Satırlar
+ label_saving: Kaydediliyor ...
+ label_sum: Toplam
+ label_yes: Evet
+ load_query_question: 'Report will have %{size} table cells and may take some time to render. Do you still want to try rendering it?'
+ units: Birimler
+ validation_failure_date: geçerli bir tarih değil
+ validation_failure_integer: geçerli bir tamsayı değil
diff --git a/vendored-plugins/reporting_engine/doc/API_CHANGES.md b/vendored-plugins/reporting_engine/doc/API_CHANGES.md
new file mode 100644
index 0000000000..991acacda6
--- /dev/null
+++ b/vendored-plugins/reporting_engine/doc/API_CHANGES.md
@@ -0,0 +1,24 @@
+
+
+# Rails 3.2 Upgrade
+
+* add Gem dependency
+* require 'reporting_engine/x' instead of 'x'
diff --git a/vendored-plugins/reporting_engine/doc/COPYRIGHT.md b/vendored-plugins/reporting_engine/doc/COPYRIGHT.md
new file mode 100644
index 0000000000..90d044ff54
--- /dev/null
+++ b/vendored-plugins/reporting_engine/doc/COPYRIGHT.md
@@ -0,0 +1,18 @@
+ReportingEngine
+
+A Rails engine allowing to create customized database reports
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/reporting_engine/doc/COPYRIGHT_short.md b/vendored-plugins/reporting_engine/doc/COPYRIGHT_short.md
new file mode 100644
index 0000000000..5883789471
--- /dev/null
+++ b/vendored-plugins/reporting_engine/doc/COPYRIGHT_short.md
@@ -0,0 +1,16 @@
+ReportingEngine
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
diff --git a/vendored-plugins/reporting_engine/doc/GPL.txt b/vendored-plugins/reporting_engine/doc/GPL.txt
new file mode 100644
index 0000000000..94a9ed024d
--- /dev/null
+++ b/vendored-plugins/reporting_engine/doc/GPL.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/vendored-plugins/reporting_engine/frontend/app/reporting_engine-app.js b/vendored-plugins/reporting_engine/frontend/app/reporting_engine-app.js
new file mode 100644
index 0000000000..678b3841f4
--- /dev/null
+++ b/vendored-plugins/reporting_engine/frontend/app/reporting_engine-app.js
@@ -0,0 +1,34 @@
+//-- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+//++
+
+// load all js locales
+var localeFiles = require.context('../../config/locales', false, /js-[\w|-]{2,5}\.yml$/);
+localeFiles.keys().forEach(function(localeFile) {
+ var locale = localeFile.match(/js-([\w|-]{2,5})\.yml/)[1];
+ I18n.addTranslations(locale, localeFiles(localeFile)[locale]);
+});
\ No newline at end of file
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/LICENCE.md b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/LICENCE.md
new file mode 100644
index 0000000000..4137f05538
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/LICENCE.md
@@ -0,0 +1,24 @@
+# All images in this directory are lincensed under the Creative Commons License
+
+Some images are from the "Minicons Free Vector Icons Pack by Webalys",
+some from the "Minimal Vector Icons by Webalys". Detailed license for both icon sets follow.
+
+## Minicons
+
+
+Minicons Free Vector Icons Pack by Webalys is licensed under a Creative Commons Attribution 3.0 Unported License and Free for both personal and commercial use. You can copy, adapt, remix, distribute or transmit it.
+
+Under this condition: provide a mention of this "Minicons Free Vector Icons Pack" and a link back to this page: http://www.webalys.com/minicons
+
+
+License: http://www.webalys.com/minicons/license.php
+
+## Minimal Vector Icons
+
+
+This Pack is published under a Creative Commons Attribution license and Free for both personal and commercial use. You can copy, adapt, remix, distribute or transmit it.
+
+Under this condition: provide a mention of this "User Interface Design framework" and a link back to this page: http://www.webalys.com/design-interface-application-framework.php
+
+
+License: https://creativecommons.org/licenses/by/3.0/
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/close.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/close.gif
new file mode 100644
index 0000000000..f072eb5fc6
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/close.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/delete.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/delete.gif
new file mode 100644
index 0000000000..7667901311
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/delete.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disk.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disk.gif
new file mode 100644
index 0000000000..7e56515ad2
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disk.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disks.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disks.gif
new file mode 100644
index 0000000000..df582582cb
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/disks.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/icon_info_red.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/icon_info_red.gif
new file mode 100644
index 0000000000..34de8c6b98
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/icon_info_red.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/loading.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/loading.gif
new file mode 100644
index 0000000000..085ccaecaf
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/loading.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/remove.gif b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/remove.gif
new file mode 100644
index 0000000000..0ac6342b7e
Binary files /dev/null and b/vendored-plugins/reporting_engine/lib/assets/images/reporting_engine/remove.gif differ
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting.js
new file mode 100644
index 0000000000..7a6a73ca5a
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting.js
@@ -0,0 +1,67 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Element */
+
+window.Reporting = {
+ onload: function (func) {
+ document.observe("dom:loaded", func);
+ },
+
+ flash: function (string, type) {
+ if (type === undefined) {
+ type = "error";
+ }
+
+ if ($("flash_" + type) !== null) {
+ $("flash_" + type).remove();
+ }
+
+ var flash = new Element('div', {
+ 'id': 'flash_' + type,
+ 'class': 'flash ' + type
+ }).update(new Element('a', {
+ 'href': '#'
+ }).update(string));
+
+ $("content").insert({top: flash});
+ $$("#flash_" + type + " a")[0].focus();
+ },
+
+ clearFlash: function () {
+ $$('div[id^=flash]').each(function (oldMsg) {
+ oldMsg.remove();
+ });
+ },
+
+ fireEvent: function (element, event) {
+ var evt;
+ if (document.createEventObject) {
+ // dispatch for IE
+ evt = document.createEventObject();
+ return element.fireEvent('on' + event, evt);
+ } else {
+ // dispatch for firefox + others
+ evt = document.createEvent("HTMLEvents");
+ evt.initEvent(event, true, true); // event type,bubbling,cancelable
+ return !element.dispatchEvent(evt);
+ }
+ }
+};
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/controls.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/controls.js
new file mode 100644
index 0000000000..ee8ac97e53
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/controls.js
@@ -0,0 +1,191 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Effect, Ajax, Element, selectAllOptions, Form */
+
+Reporting.Controls = {
+ query_name_editor: function (target_id) {
+ var target = $(target_id);
+ var isPublic = target.getAttribute("data-is_public") === "true";
+ var updateUrl = target.getAttribute("data-update-url");
+ var translations = target.getAttribute("data-translations");
+ if (translations.isJSON()) {
+ translations = translations.evalJSON(true);
+ }
+ if (translations === undefined) {
+ translations = {};
+ }
+ if (translations.rename === undefined) {
+ translations.rename = 'ok';
+ }
+ if (translations.cancel === undefined) {
+ translations.cancel = 'cancel';
+ }
+ if (translations.saving === undefined) {
+ translations.saving = 'Saving...';
+ }
+ if (translations.loading === undefined) {
+ translations.loading = 'Loading...';
+ }
+ if (translations.clickToEdit === undefined) {
+ translations.loading = 'Click to edit';
+ }
+ },
+
+ toggle_delete_form: function (e) {
+ var offset = $('query-icon-delete').positionedOffset().left;
+ $('delete_form').setStyle("left: " + offset + "px").toggle();
+ e.preventDefault();
+ },
+
+ toggle_save_as_form: function (e) {
+ var offset = $('query-icon-save-as').positionedOffset().left;
+ $('save_as_form').setStyle("left: " + offset + "px").toggle();
+ e.preventDefault();
+ },
+
+ clear_query: function (e) {
+ Reporting.Filters.clear();
+ Reporting.GroupBys.clear();
+ e.preventDefault();
+ },
+
+ send_settings_data: function (targetUrl, callback, failureCallback) {
+ if (failureCallback === undefined) {
+ failureCallback = Reporting.Controls.default_failure_callback;
+ }
+ Reporting.clearFlash();
+ new Ajax.Request(
+ targetUrl,
+ { asynchronous: true,
+ evalScripts: true,
+ postBody: Reporting.Controls.serialize_settings_form(),
+ onSuccess: callback,
+ onFailure: failureCallback });
+ },
+
+ serialize_settings_form: function() {
+ var ret_str, grouping_str;
+ // prototype has a bug when trying to serialize arrays with multiple values, hence jQuery
+ // see https://prototype.lighthouseapp.com/projects/8886/tickets/1180-formserialize-is-broken-for-multiple-selects
+ ret_str = jQuery('#query_form').serialize();
+ grouping_str = $w('rows columns').inject('', function(grouping, type) {
+ return grouping + $('group_by_' + type).select('.group_by_element').map(function(group_by) {
+ return 'groups[' + type + '][]=' + group_by.readAttribute('data-group-by');
+ }).inject('', function(all_group_str, group_str) {
+ return all_group_str + '&' + group_str;
+ });
+ });
+ if (grouping_str.length > 0) {
+ ret_str += grouping_str;
+ }
+ return ret_str;
+ },
+
+ attach_settings_callback: function (element, callback) {
+ if (element === null) {
+ return;
+ }
+ failureCallback = function (response) {
+ $('result-table').update("");
+ Reporting.Controls.default_failure_callback(response);
+ };
+ element.observe("click", function (e) {
+ Reporting.Controls.send_settings_data(this.getAttribute("data-target"), callback, failureCallback);
+ e.preventDefault();
+ });
+ },
+
+ observe_click: function (element_id, callback) {
+ var el = $(element_id);
+ if (el !== null && el !== undefined) {
+ el.observe("click", callback);
+ }
+ },
+
+ update_result_table: function (response) {
+ $('result-table').update(response.responseText);
+ Reporting.Progress.confirm_question();
+ },
+
+ default_failure_callback: function (response) {
+ if (response.status >= 400 && response.status < 500) {
+ Reporting.flash(response.responseText);
+ Reporting.Progress.abort();
+ } else {
+ Reporting.flash(I18n.t("js.reporting_engine.label_response_error"));
+ Reporting.Progress.abort();
+ }
+ },
+
+ update_report_lists: function () {
+ $$(".report_list").each(function (list) {
+ Reporting.Controls.update_report_list(list);
+ });
+ },
+
+ update_report_list: function (list) {
+ var url = $(list).readAttribute("data-update-url");
+ if (url == null) {
+ return;
+ }
+ new Ajax.Request(url, {
+ onSuccess: function (response) {
+ list.replace(response.responseText);
+ }
+ });
+ }
+};
+
+Reporting.onload(function () {
+ if ($('query_saved_name') !== null) {
+ if ($('query_saved_name').getAttribute("data-update-url") !== null) {
+ Reporting.Controls.query_name_editor('query_saved_name');
+ }
+ // don't concern ourselves with new queries
+ if ($('query_saved_name').getAttribute("data-is_new") !== null) {
+ if ($('query-icon-delete') !== null) {
+ Reporting.Controls.observe_click("query-icon-delete", Reporting.Controls.toggle_delete_form);
+ Reporting.Controls.observe_click("query-icon-delete-cancel", Reporting.Controls.toggle_delete_form);
+ $('delete_form').hide();
+ }
+
+ if ($("query-breadcrumb-save") !== null) {
+ // When saving an update of an exisiting query or apply filters, we replace the table on success
+ Reporting.Controls.attach_settings_callback($("query-breadcrumb-save"), Reporting.Controls.update_result_table);
+ }
+ }
+ }
+
+ Reporting.Controls.observe_click("query-icon-save-as", Reporting.Controls.toggle_save_as_form);
+ Reporting.Controls.observe_click("query-icon-save-as-cancel", Reporting.Controls.toggle_save_as_form);
+ if ($('save_as_form') !== null) {
+ $('save_as_form').hide();
+ }
+
+ // When saving a new query, the success-response is the new saved query's url -> redirect to that
+ Reporting.Controls.attach_settings_callback($("query-icon-save-button"), function (response) {
+ Ajax.activeRequestCount = Ajax.activeRequestCount + 1; // HACK: Prevent Loading spinner from disappearing
+ document.location = response.responseText;
+ });
+ // When saving an update of an exisiting query or apply filters, we replace the table on success
+ Reporting.Controls.attach_settings_callback($("query-icon-apply-button"), Reporting.Controls.update_result_table);
+ Reporting.Controls.observe_click($('query-link-clear'), Reporting.Controls.clear_query);
+});
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/filters.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/filters.js
new file mode 100644
index 0000000000..75c6c4ab0d
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/filters.js
@@ -0,0 +1,544 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Effect, Ajax, Element, Form */
+
+Reporting.Filters = {
+ load_available_values_for_filter: function (filter_name, callback_func) {
+ var select, radio_options, post_select_values;
+ select = $$('.filter-value[data-filter-name="' + filter_name + '"]').first();
+ // check if we might have a radio-box
+ radio_options = $$('.' + filter_name + '_radio_option input');
+ if (radio_options && radio_options.size() !== 0) {
+ radio_options.first().checked = true;
+ callback_func();
+ }
+ if (select === null || select === undefined) {
+ return;
+ }
+ url = select.readAttribute("data-remote-url");
+ json_post_select_values = select.readAttribute('data-initially-selected');
+ if (json_post_select_values !== null && json_post_select_values !== undefined) {
+ post_select_values = json_post_select_values.replace(/'/g, '"').evalJSON(true);
+ }
+ if (select.readAttribute('data-loading') === "ajax" && select.childElements().length === 0) {
+ if (window.global_prefix === undefined) {
+ window.global_prefix = "";
+ }
+
+ new Ajax.Updater({ success: select.id }, url, {
+ parameters: {
+ filter_name: filter_name,
+ values: json_post_select_values
+ },
+ insertion: 'bottom',
+ evalScripts: false,
+ onCreate: function (a, b) {
+ $$("select[data-filter-name='" + filter_name + "']").each(function (e) { e.disable(); });
+ },
+ onComplete: function (a, b) {
+ $$("select[data-filter-name='" + filter_name + "']").each(function (e) { e.enable(); });
+ if (select.tagName.toLowerCase() === "select") {
+ if (post_select_values === undefined || post_select_values === null || post_select_values.size() === 0) {
+ select.selectedIndex = 0;
+ } else {
+ Reporting.Filters.select_values(select, post_select_values);
+ }
+ }
+ callback_func();
+ }
+ });
+ Reporting.Filters.multi_select(select, false);
+ } else {
+ callback_func();
+ }
+ },
+
+ show_filter: function (field, options) {
+ if (options === undefined) {
+ options = {};
+ }
+ if (options.callback_func === undefined) {
+ options.callback_func = function () {};
+ }
+ if (options.slowly === undefined) {
+ options.slowly = false;
+ }
+ if (options.show_filter === undefined) {
+ options.show_filter = true;
+ }
+ if (options.hide_only === undefined) {
+ options.hide_only = false;
+ }
+ var field_el = $('filter_' + field);
+ if (field_el !== null) {
+ if (options.insert_after === undefined) {
+ options.insert_after = Reporting.Filters.last_visible_filter();
+ }
+ if (options.insert_after !== undefined && options.show_filter) {
+ // Move the filter down to appear after the last currently visible filter
+ if (field_el.id !== options.insert_after.id) {
+ field_el.remove();
+ options.insert_after.insert({after: field_el});
+ }
+ }
+ // the following command might be included into the callback_function (which is called after the ajax request) later
+ var display_functor;
+ if (options.show_filter) {
+ (options.slowly ? Effect.Appear : Element.show)(field_el);
+ Reporting.Filters.load_available_values_for_filter(field, options.callback_func);
+ $('rm_' + field).value = field; // set the value, so the serialized form will return this filter
+ Reporting.Filters.value_changed(field);
+ Reporting.Filters.set_filter_value_widths(100);
+ } else {
+ (options.slowly ? Effect.Fade : Element.hide)(field_el);
+ if (!options.hide_only) { // remember that this filter used to be selected
+ field_el.removeAttribute('data-selected');
+ }
+ $('rm_' + field).value = ""; // reset the value, so the serialized form will not return this filter
+ Reporting.Filters.set_filter_value_widths(5000);
+ }
+ Reporting.Filters.operator_changed(field, $("operators[" + field + "]"));
+ Reporting.Filters.display_category($(field_el.getAttribute("data-label")));
+ }
+ },
+
+ /**
+ * Activates the filter with the given name and loads dependent filters if necessary.
+ *
+ * @param filter_name Name of the filter to be activated.
+ */
+ add_filter: function (filter_name, activate_dependent, on_complete) {
+ var field = filter_name
+ if (activate_dependent === undefined) {
+ activate_dependent = true;
+ }
+ if (on_complete === undefined) {
+ on_complete = function() { };
+ }
+ // do this immediately instead of in callback to avoid concurrency issues during testing
+ Reporting.Filters.select_option_enabled($("add_filter_select"), filter_name, false);
+ Reporting.Filters.show_filter(field, { slowly: true, callback_func: function() {
+ if (activate_dependent) {
+ Reporting.Filters.activate_dependents($(field + "_arg_1_val"));
+ }
+ on_complete();
+ }
+ });
+ },
+
+ remove_filter: function (field, hide_only) {
+ Reporting.Filters.show_filter(field, { show_filter: false, hide_only: hide_only });
+ var dependent = Reporting.Filters.get_dependents($(field + '_arg_1_val'), false).find(function(d) {
+ return Reporting.Filters.visible_filters().include(d);
+ });
+ if (dependent !== undefined) {
+ Reporting.Filters.remove_filter(dependent);
+ }
+ Reporting.Filters.select_option_enabled($("add_filter_select"), field, true);
+ },
+
+ /*
+ Smoothly sets the width of currently displayed filters.
+ Params:
+ delay:Int
+ Time to wait before resizing the filters width */
+ set_filter_value_widths: function (delay) {
+ window.clearTimeout(Reporting.Filters.set_filter_value_widths_timeout);
+ if (Reporting.Filters.visible_filters().size() > 0) {
+ Reporting.Filters.set_filter_value_widths_timeout = window.setTimeout(function () {
+ var table_data = $("filter_" + Reporting.Filters.visible_filters().first()).select(".advanced-filters--filter-value").first().up();
+ var current_width = table_data.getWidth();
+ var filter_values = $($$(".advanced-filters--filter-value"));
+ // First, reset all widths
+ filter_values.each(function (f) {
+ $(f).up().style.width = "auto";
+ });
+ // Now, get the current width
+ // Any width will be fine, as the table layout makes all elements the same width
+ var new_width = table_data.getWidth();
+ if (new_width < current_width) {
+ // Set all widths to previous, so we can animate
+ filter_values.each(function (f) {
+ $(f).up().style.width = current_width + "px";
+ });
+ }
+ // Now, set all widths to be the widest
+ filter_values.each(function (f) {
+ if (new_width < current_width) {
+ $(f).up().morph("width: " + new_width + "px;");
+ } else {
+ $(f).up().style.width = new_width + "px";
+ }
+ });
+ }, delay);
+ }
+ },
+ set_filter_value_widths_timeout: undefined,
+
+ last_visible_filter: function () {
+ return $($$('.filter')).reverse().detect(function (f) {
+ return f.visible();
+ });
+ },
+
+ /* Display the given category if any of its filters are visible. Otherwise hide it */
+ display_category: function (label) {
+ if (label !== null) {
+ var filters = $$('.filter');
+ for (var i = 0; i < filters.length; i += 1) {
+ if (filters[i].visible() && filters[i].getAttribute("data-label") === label) {
+ Element.show(label);
+ return;
+ }
+ }
+ Element.hide(label);
+ }
+ },
+
+ operator_changed: function (field, select) {
+ var option_tag, arity, first;
+ if (select === null) {
+ return;
+ }
+ first = false
+ if (select.getAttribute("data-first") === undefined || select.getAttribute("data-first") === null) {
+ first = true;
+ $(select).setAttribute("data-first", "false");
+ }
+ option_tag = select.options[select.selectedIndex];
+ arity = parseInt(option_tag.getAttribute("data-arity"), 10);
+ Reporting.Filters.change_argument_visibility(field, arity);
+ if (option_tag.getAttribute("data-forced") !== undefined && option_tag.getAttribute("data-forced") !== null) {
+ Reporting.Filters.force_type(option_tag, first);
+ };
+ },
+
+ // Overwrite to customize input enforcements.
+ // option: 'option' HTMLElement
+ // first: Boolean indicating whether the operator changed for the first time
+ force_type: function (option, first) {
+ true;
+ },
+
+ value_changed: function (field) {
+ var val, filter;
+ val = $(field + '_arg_1_val');
+ filter = $('filter_' + field);
+ if (!val) {
+ return;
+ }
+ if (val.value === '<>') {
+ filter.addClassName('inactive-filter');
+ } else {
+ filter.removeClassName('inactive-filter');
+ }
+ },
+
+ change_argument_visibility: function (field, arg_nr) {
+ var params, i;
+ params = [$(field + '_arg_1'), $(field + '_arg_2')];
+
+ for (i = 0; i < 2; i += 1) {
+ if (params[i] !== null) {
+ if (arg_nr >= (i + 1) || arg_nr <= (-1 - i)) {
+ params[i].show();
+ params[i].descendants().each(function (desc) { desc.show(); });
+ } else {
+ params[i].hide();
+ params[i].descendants().each(function (desc) { desc.hide(); });
+ }
+ }
+ }
+ },
+
+ select_option_enabled: function (box, value, state) {
+ var option = box.select("[value='" + value + "']").first();
+ if (option !== undefined) {
+ option.disabled = !state;
+ }
+ },
+
+ multi_select: function (select, multi) {
+ select.multiple = multi;
+ if (multi) {
+ select.size = 4;
+ // deselect first option if it's present
+ if (select.options[0] !== undefined && select.options[0] !== null) {
+ select.options[0].selected = false;
+ }
+ } else {
+ select.size = 1;
+ }
+ },
+
+ toggle_multi_select: function (select) {
+ Reporting.Filters.multi_select(select, !select.multiple);
+ },
+
+ visible_filters: function () {
+ return $("filter_table").select(".advanced-filters--filter").select(function (filter) {
+ return filter.visible() === true;
+ }).collect(function (filter) {
+ return filter.getAttribute("data-filter-name");
+ });
+ },
+
+ clear: function () {
+ Reporting.Filters.visible_filters().each(function (filter) {
+ Reporting.Filters.remove_filter(filter);
+ });
+ },
+
+ // Returns an array of dependents of the given element
+ // get_all -> Boolean: whether to return all dependends (even the
+ // dependents of this filters dependents) or not
+ get_dependents: function (element, get_all) {
+ var dependent_field = "data-all-dependents";
+ if (get_all === false) {
+ dependent_field = "data-next-dependents";
+ }
+ if (element.hasAttribute(dependent_field)) {
+ return element.getAttribute(dependent_field).replace(/'/g, '"').evalJSON(true);
+ } else {
+ return [];
+ }
+ },
+
+ // Activate the first dependent of the changed filter, if it is not already active.
+ // Afterwards, collect the visible filters from the dependents list and start
+ // narrowing down their values.
+ // Param: select [optional] - the select-box of the filter which should activate it's dependents
+ activate_dependents: function (selectBox, callbackWhenFinished) {
+ var all_dependents, next_dependents, dependent, active_filters, source;
+ if (selectBox === undefined || (selectBox.type && selectBox.type.toLowerCase() == 'change')) {
+ selectBox = this;
+ }
+ if (selectBox.tagName.toLowerCase() !== "select") {
+ return; // only multi_value filters have dependents
+ }
+ if (callbackWhenFinished === undefined) {
+ callbackWhenFinished = function(dependent) { };
+ }
+ source = selectBox.getAttribute("data-filter-name");
+ all_dependents = Reporting.Filters.get_dependents(selectBox);
+ next_dependents = Reporting.Filters.get_dependents(selectBox, false);
+ dependent = Reporting.Filters.which_dependent_shall_i_take(source, next_dependents);
+ if (!dependent) {
+ return;
+ }
+ active_filters = Reporting.Filters.visible_filters();
+
+ if (!active_filters.include(dependent)) {
+ // in case we run into a situation where the dependent to show is not in the currently selected dependency chain
+ // we have to remove all filters until we reach the source and add the new dependent
+ if (next_dependents.any( function(d){ return active_filters.include(d) } )) {
+ while (active_filters.last() !== source) {
+ Reporting.Filters.show_filter(active_filters.pop(1), { show_filter: false, slowly: true });
+ }
+ }
+ Reporting.Filters.show_filter(dependent, { slowly: true, insert_after: $(selectBox.up(".filter")) });
+ // render filter inactive if possible to avoid unintended filtering
+ $(dependent + '_arg_1_val').value = '<>'
+ Reporting.Filters.operator_changed(dependent, $('operators[' + dependent + ']'));
+ // Hide remove box of dependent
+ $('rm_box_' + dependent).hide();
+ // Remove border of dependent, so it "merges" with the filter before
+ $('filter_' + dependent).addClassName("no-border");
+ active_filters.unshift(dependent);
+ }
+ setTimeout(function () { // Make sure the newly shown filters are in the DOM
+ var active_dependents = all_dependents.select(function (d) {
+ return active_filters.include(d);
+ });
+ Reporting.Filters.narrow_values(
+ Reporting.Filters.dependent_for(source),
+ active_dependents,
+ function() { callbackWhenFinished(dependent); }
+ );
+ }, 1);
+ },
+
+ // return an array of all filters that depend on the given filter plus the given filter
+ dependent_for: function(field) {
+ var deps = $$('.advanced-filters--select[data-all-dependents]').findAll(function(selectBox) {
+ return (selectBox.up('li').visible()) && Reporting.Filters.get_dependents(selectBox).include(field)
+ }).map(function(selectBox) {
+ return selectBox.getAttribute("data-filter-name");
+ });
+ return deps === undefined ? [ field ] : [ field ].concat(deps)
+ },
+
+ // Select the given values of the selectBox.
+ // Toggle multi-select state of the selectBox depending on how many values were given.
+ select_values: function(selectBox, values_to_select) {
+ Reporting.Filters.multi_select(selectBox, values_to_select.size() > 1);
+ values_to_select.each(function (val) {
+ var opt = selectBox.select("option[value='" + val + "']");
+ if (opt.size() > 0) {
+ opt.first().selected = true;
+ }
+ });
+ },
+
+ exists: function (filter) {
+ return Reporting.Filters.visible_filters().include(filter);
+ },
+
+ // Narrow down the available values for the [dependents] of [sources].
+ // This will narrow down for each dependent separately, adding each finished
+ // dependent to the sources array and removing it from the dependents array.
+ narrow_values: function (sources, dependents, callbackWhenFinished) {
+ if (sources.size() === 0 || dependents.size === 0 || dependents.first() === undefined) {
+ return;
+ }
+ if (callbackWhenFinished === undefined) {
+ callbackWhenFinished = function() {};
+ }
+ var params = document.location.href.include('?') ? '&' : '?'
+ params = params + "narrow_values=1&dependent=" + dependents.first();
+ sources.each(function (filter) {
+ params = params + "&sources[]=" + filter;
+ });
+ var targetUrl = document.location.href + params;
+ var currentDependent = dependents.first();
+ var updater = new Ajax.Request(targetUrl,
+ {
+ asynchronous: true,
+ evalScripts: true,
+ postBody: Reporting.Controls.serialize_settings_form(),
+ onSuccess: function (response) {
+ Reporting.clearFlash();
+ if (response.responseJSON !== undefined) {
+ var continue_narrowing = true;
+ var selectBox = $(currentDependent + "_arg_1_val");
+ var selected = selectBox.select("option").collect(function (sel) {
+ if (sel.selected) {
+ return sel.value;
+ }
+ }).compact();
+ // remove old values
+ $(selectBox).childElements().each(function (o) {
+ o.remove();
+ });
+ // insert new values
+ response.responseJSON.each(function (o) {
+ var ary = [ (o === null ? "" : o) ].flatten();
+ var label = ary.first();
+ var value = ary.last();
+ // cannot use .innerhtml due to IE wierdness
+ $(selectBox).insert(new Element('option', {value: value}).update(label.escapeHTML()));
+ });
+
+ Reporting.Filters.select_values(selectBox, selected);
+
+ sources.push(currentDependent); // Add as last element
+ dependents.splice(0, 1); // Delete first element
+ // if we got no values besides the <> value, do not show this selectBox
+ if (!selectBox.select("option").any(function (opt) { return opt.value != '<>' })) {
+ Reporting.Filters.show_filter(currentDependent, { show_filter: false });
+ continue_narrowing = false;
+ }
+ // if the current filter is inactive, hide dependent - otherwise recurisvely narrow dependent values
+ if (selectBox.value == '<>') {
+ Reporting.Filters.value_changed(currentDependent);
+ dependents.each(function (dependent) {
+ Reporting.Filters.show_filter(dependent, {
+ slowly: true,
+ show_filter: false });
+ });
+ continue_narrowing = false;
+ }
+ if (continue_narrowing) {
+ Reporting.Filters.narrow_values(sources, dependents);
+ }
+ callbackWhenFinished();
+ }
+ },
+ onException: function (response, error) {
+ if (console) {
+ console.log(error);
+ }
+ Reporting.flash("Loading of filter values failed. Probably, the server is temporary offline for maintenance.");
+ var selectBox = $(currentDependent + "_arg_1_val");
+ $(selectBox).insert(new Element('option', {value: '<>'}).update('Failed to load values.'));
+ }
+ }
+ );
+ },
+
+ // This method may be overridden by the actual application to define custon behavior
+ // If there are multiple possible dependents to follow.
+ // The dependent to follow should be returned.
+ which_dependent_shall_i_take: function(source, dependents) {
+ return dependents.first();
+ }
+};
+
+Reporting.onload(function () {
+ if ($("add_filter_select")) {
+ $("add_filter_select").observe("change", function () {
+ if (!(Reporting.Filters.exists(this.value))) {
+ Reporting.Filters.add_filter(this.value);
+ this.selectedIndex = 0;
+ };
+ });
+ }
+
+ $$(".filter_rem").each(function (e) {
+ e.observe("click", function (event) {
+ Event.stop(event);
+ var filter_name = this.up('li').getAttribute("data-filter-name");
+ Reporting.Filters.remove_filter(filter_name);
+ }).observe("keydown", function (event) {
+ if (event.keyCode == 13 || event.keyCode == 32) {
+ Event.stop(event);
+ var filter_name = this.up('li').getAttribute("data-filter-name");
+ Reporting.Filters.remove_filter(filter_name);
+ $$('#filters > legend a')[0].focus();
+ }
+ });
+ });
+ $$(".filter_operator").each(function (e) {
+ e.observe("change", function (evt) {
+ var filter_name = this.getAttribute("data-filter-name");
+ Reporting.Filters.operator_changed(filter_name, this);
+ Reporting.fireEvent($(filter_name + "_arg_1_val"), "change");
+ });
+ });
+ $$(".filter_multi-select").each(function (e) {
+ e.observe("click", function () {
+ Reporting.Filters.toggle_multi_select($(this.getAttribute("data-filter-name") + '_arg_1_val'));
+ });
+ });
+ $$(".advanced-filters--select").each(function (s) {
+ var selected_size = Array.from(s.options).findAll(function (o) {
+ return o.selected === true;
+ }).size();
+ s.multiple = (selected_size > 1);
+ s.observe("change", function (evt) {
+ var filter_name = this.up('li').getAttribute("data-filter-name");
+ Reporting.Filters.value_changed(filter_name);
+ });
+ });
+ $$('.advanced-filters--select[data-all-dependents]').each(function (dependency) {
+ dependency.observe("change", Reporting.Filters.activate_dependents);
+ });
+});
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/group_bys.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/group_bys.js
new file mode 100644
index 0000000000..cb6175751f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/group_bys.js
@@ -0,0 +1,226 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Effect, Ajax, selectAllOptions, moveOptions, moveOptionUp, moveOptionDown */
+
+Reporting.GroupBys = {
+ group_by_container_ids: function() {
+ var ids = $w('group_by_columns group_by_rows');
+ return ids.select(function (i) {
+ return $(i) !== null
+ });
+ },
+
+ sortable_options: function() {
+ return {
+ tag: 'span',
+ only: "drag_element",
+ overlap: 'horizontal',
+ constraint:'horizontal',
+ containment: Reporting.GroupBys.group_by_container_ids(),
+ dropOnEmpty: true,
+ hoverclass: 'drag_container_accept'
+ };
+ },
+
+ recreate_sortables: function() {
+ Reporting.GroupBys.group_by_container_ids().each(function(id) {
+ Sortable.create(id, Reporting.GroupBys.sortable_options());
+ });
+ },
+
+ initialize_drag_and_drop_areas: function() {
+ Reporting.GroupBys.recreate_sortables();
+ },
+
+ create_label: function(group_by, text) {
+ return new Element('label', {
+ 'class': 'in_row group_by_label',
+ 'for': group_by.identify(),
+ 'id': group_by.identify() + '_label'
+ }).update(text);
+ },
+
+ create_remove_button: function(group_by) {
+ var remove_link, remove_icon;
+ remove_link = new Element('a', {
+ 'class': 'group_by_remove in_row',
+ 'id': group_by.identify() + '_remove',
+ 'href': 'javascript:'
+ });
+ remove_icon = $('hidden_remove_img').clone();
+ remove_icon.removeAttribute('id');
+ remove_icon.removeAttribute('style');
+
+ remove_link.setAttribute('title', I18n.t("js.reporting_engine.label_remove") + ' ' + group_by.down('label').innerHTML);
+ remove_icon.setAttribute('alt', I18n.t("js.reporting_engine.label_remove") + ' ' + group_by.down('label').innerHTML);
+
+ remove_link.observe('click', function(e) {
+ Reporting.GroupBys.remove_element_event_action(e, group_by, remove_link)
+ });
+ remove_link.observe('keypress', function(e) {
+ /* keyCode 32: Space */
+ if (e.keyCode == 32) {
+ e.preventDefault();
+ Reporting.GroupBys.remove_element_event_action(e, group_by, remove_link)
+ }
+ });
+ remove_link.appendChild(remove_icon);
+ return remove_link;
+ },
+
+ remove_element_event_action: function(event, group_by, button) {
+ var node;
+ if (node = group_by.next('span')) {
+ node = node.down('a');
+ if (node) {
+ node.focus();
+ }
+ }
+ else if (node = group_by.next('select')) {
+ node.focus();
+ }
+ Reporting.GroupBys.remove_group_by(button.up('.group_by_element'));
+ },
+
+ create_arrow: function(group_by, position) {
+ return new Element('span', {
+ 'class': 'arrow in_row arrow_' + position,
+ 'id': group_by.identify() + '_arrow_' + position
+ });
+ },
+
+ create_group_by: function(field, caption) {
+ var group_by, label, right_arrow, left_arrow, remove_button;
+ group_by = new Element('span', {
+ 'class': 'in_row drag_element group_by_element',
+ 'data-group-by': field
+ });
+ group_by.identify(); // give it a unique id
+
+ left_arrow = Reporting.GroupBys.create_arrow(group_by, 'left');
+ group_by.appendChild(left_arrow);
+
+ label = Reporting.GroupBys.create_label(group_by, caption);
+ Reporting.GroupBys.init_group_by_hover_effects([group_by, label]);
+ group_by.appendChild(label);
+
+ remove_button = Reporting.GroupBys.create_remove_button(group_by);
+ group_by.appendChild(remove_button);
+
+ right_arrow = Reporting.GroupBys.create_arrow(group_by, 'right');
+ group_by.appendChild(right_arrow);
+ return group_by;
+ },
+
+ // on mouse_over of a group_by or it's label, change the color of the group_by
+ // also change the color of the arrows
+ init_group_by_hover_effects: function(elements) {
+ elements.each(function(element) {
+ ['mouseover', 'mouseout'].each(function(event_type) {
+ element.observe(event_type, function(event) {
+ Reporting.GroupBys.group_by_hover_effect(event, event_type == 'mouseover');
+ });
+ });
+ });
+ },
+
+ group_by_hover_effect: function(event, do_hover) {
+ var group_by = $(Event.element(event));
+ // we possibly hit a tag inside the group_by, so go search the group_by then
+ if (!group_by.hasClassName('group_by_element')) {
+ group_by = group_by.up('.group_by_element');
+ }
+ if (group_by !== null) {
+ Reporting.GroupBys.group_by_hover(group_by, do_hover);
+ }
+ },
+
+ group_by_hover: function(group_by, state) {
+ if (state) {
+ group_by.childElements().each(function(e) { e.addClassName('hover'); });
+ } else {
+ group_by.childElements().each(function(e) { e.removeClassName('hover'); });
+ }
+ },
+
+ // This is whether it is possible to add a new group if <> through the
+ // add-group-by select-box or not.
+ adding_group_by_enabled: function(field, state) {
+ $w('add_group_by_columns add_group_by_rows').each(function(container_id) {
+ Reporting.Filters.select_option_enabled($(container_id), field, state);
+ });
+ },
+
+ remove_group_by: function(group_by) {
+ Reporting.GroupBys.adding_group_by_enabled(group_by.readAttribute('data-group-by'), true);
+ group_by.remove();
+ },
+
+ add_group_by_from_select: function(select) {
+ var field, caption, container, selected_option;
+ field = $(select).getValue();
+ container = select.up('.drag_container');
+ selected_option = select.select("[value='" + field + "']").first();
+ caption = selected_option.readAttribute('data-label');
+ Reporting.GroupBys.add_group_by(field, caption, container);
+ select.select("[value='']").first().selected = true;
+ },
+
+ add_group_by: function(field, caption, container) {
+ var group_by, add_groups_select_box;
+ add_groups_select_box = container.select('select').first();
+ group_by = Reporting.GroupBys.create_group_by(field, caption);
+ add_groups_select_box.insert({ before: group_by });
+ Reporting.GroupBys.adding_group_by_enabled(field, false);
+ Reporting.GroupBys.recreate_sortables();
+ },
+
+ clear: function() {
+ Reporting.GroupBys.visible_group_bys().each(function (group_by) {
+ Reporting.GroupBys.remove_group_by(group_by);
+ });
+ },
+
+ visible_group_bys: function() {
+ return Reporting.GroupBys.group_by_container_ids().collect(function (container) {
+ return $(container).select('[data-group-by]')
+ }).flatten();
+ },
+
+ exists: function(group_by_name) {
+ return Reporting.GroupBys.visible_group_bys().any(function (grp) {
+ return grp.getAttribute('data-group-by') == group_by_name;
+ });
+ }
+};
+
+Reporting.onload(function () {
+ Reporting.GroupBys.initialize_drag_and_drop_areas();
+ [$('add_group_by_rows'), $('add_group_by_columns')].each(function (select) {
+ if (select !== null) {
+ select.observe("change", function () {
+ if (!(Reporting.GroupBys.exists(this.value))) {
+ Reporting.GroupBys.add_group_by_from_select(this);
+ };
+ });
+ }
+ });
+});
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/progressbar.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/progressbar.js
new file mode 100644
index 0000000000..85c9a06ca6
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/progressbar.js
@@ -0,0 +1,68 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Effect, Ajax */
+
+Reporting.Progress = {
+
+ abort: function () {
+ if (window.progressbar !== undefined && window.progressbar !== null) {
+ window.progressbar.stop();
+ }
+ },
+
+ replace_with_bar: function (element) {
+ var parent = element.up();
+ var size = parseInt(element.getAttribute('data-query-size'), 10) || 500;
+ element.remove();
+ window.progressbar = Reporting.Progress.add_bar_to_parent(parent);
+ // Speed determined through laborous experimentation!
+ window.progressbar.options.interval = (size * (Math.log(size))) / 100000;
+ window.progressbar.start();
+ },
+
+ add_bar_to_parent: function (parent) {
+ parent.appendChild(new Element('div', {
+ 'id': 'progressbar_container',
+ 'class': 'progressbar_container'
+ }));
+ return new Control.ProgressBar('progressbar_container');
+ },
+
+ confirm_question: function () {
+ var bar = $('progressbar');
+ if (bar !== null && bar !== undefined) {
+ var size = bar.getAttribute('data-size');
+ var question = bar.getAttribute('data-translation');
+ if (confirm(question)) {
+ var target = bar.getAttribute("data-target");
+ bar.up().show();
+ Reporting.Progress.replace_with_bar(bar);
+ Reporting.Controls.send_settings_data(target, Reporting.Controls.update_result_table);
+ } else {
+ bar.toggle();
+ }
+ }
+ }
+};
+
+Reporting.onload(function () {
+ Reporting.Progress.confirm_question();
+});
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/restore_query.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/restore_query.js
new file mode 100644
index 0000000000..e547d9ed8f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting/restore_query.js
@@ -0,0 +1,109 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+/*jslint white: false, nomen: true, devel: true, on: true, debug: false, evil: true, onevar: false, browser: true, white: false, indent: 2 */
+/*global window, $, $$, Reporting, Effect, Ajax */
+
+Reporting.RestoreQuery = {
+
+ select_operator: function (field, operator) {
+ var select, i;
+ select = $("operators_" + field);
+ if (select === null) {
+ return; // there is no such operator select field
+ }
+ for (i = 0; i < select.options.length; i += 1) {
+ if (select.options[i].value === operator) {
+ select.selectedIndex = i;
+ break;
+ }
+ }
+ Reporting.Filters.operator_changed(field, select);
+ },
+
+ disable_select_option: function (select, field) {
+ for (var i = 0; i < select.options.length; i += 1) {
+ if (select.options[i].value === field) {
+ select.options[i].disabled = true;
+ break;
+ }
+ }
+ },
+
+ restore_dependent_filters: function(filter_name) {
+ $$("li.advanced-filters--filter[data-filter-name=" + filter_name + "] select.filter-value").each(function(selectBox) {
+ var activateNext = function(dependent) {
+ if (!dependent) return;
+ Reporting.RestoreQuery.restore_dependent_filters(dependent);
+ };
+ if (selectBox.hasAttribute('data-initially-selected')) {
+ var selected_values = selectBox.readAttribute('data-initially-selected').replace(/'/g, '"').evalJSON(true);
+ Reporting.Filters.select_values(selectBox, selected_values);
+ Reporting.Filters.value_changed(filter_name);
+ }
+ if (selectBox.getValue() !== '<>') {
+ Reporting.Filters.activate_dependents(selectBox, activateNext);
+ }
+ });
+ },
+
+ restore_filters: function () {
+ var deps = $$('.advanced-filters--select.filter-value').each(function(select) {
+ var tr = select.up('li');
+ if (tr.visible()) {
+ var filter = tr.readAttribute('data-filter-name');
+ var dependent = select.readAttribute('data-dependent');
+ if (filter && dependent) {
+ Reporting.Filters.remove_filter(filter, false);
+ }
+ }
+ });
+
+ $$("li.advanced-filters--filter[data-selected=true]").each(function (e) {
+ var select = e.down(".advanced-filters--filter-value select");
+ if (select && select.hasAttribute("data-dependent")) return;
+ var filter_name = e.getAttribute("data-filter-name");
+ var on_complete = function() {
+ Reporting.RestoreQuery.restore_dependent_filters(filter_name);
+ };
+ Reporting.Filters.add_filter(filter_name, false, on_complete);
+ });
+ },
+
+ restore_group_bys: function () {
+ Reporting.GroupBys.group_by_container_ids().each(function(id) {
+ var container, selected_groups;
+ container = $(id);
+ if (container.hasAttribute('data-initially-selected')) {
+ selected_groups = container.readAttribute('data-initially-selected').replace(/'/g, '"').evalJSON(true);
+ selected_groups.each(function(group_and_label) {
+ var group, label;
+ group = group_and_label[0];
+ label = group_and_label[1];
+ Reporting.GroupBys.add_group_by(group, label, container);
+ });
+ }
+ });
+ }
+};
+
+Reporting.onload(function () {
+ Reporting.RestoreQuery.restore_group_bys();
+ Reporting.RestoreQuery.restore_filters();
+});
diff --git a/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting_engine.js b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting_engine.js
new file mode 100644
index 0000000000..63dac46ead
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/javascripts/reporting_engine/reporting_engine.js
@@ -0,0 +1,30 @@
+//-- copyright
+// ReportingEngine
+//
+// Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//++
+
+//= require prototype
+//= require controls
+//= require reporting_engine/reporting
+//= require reporting_engine/sortable
+//= require reporting_engine/cordinc_tooltip
+//= require reporting_engine/reporting/filters
+//= require reporting_engine/reporting/group_bys
+//= require reporting_engine/reporting/restore_query
+//= require reporting_engine/reporting/controls
+//= require reporting_engine/reporting/prototype_progress_bar
+//= require reporting_engine/reporting/progressbar
diff --git a/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/help.css b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/help.css
new file mode 100644
index 0000000000..2d4e74b7ca
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/help.css
@@ -0,0 +1,76 @@
+/*-- copyright
+ReportingEngine
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++*/
+
+.help {
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.tooltip {
+ position: absolute;
+ margin-top: 3px;
+ margin-bottom: 3px;
+ padding: 3px;
+ width: 400px;
+ z-index: 256;
+ color: #000000;
+ border: 1px solid #000000;
+ background: #FFFFCC;
+ font: 12px Verdana, sans-serif;
+ text-align: left;
+ padding: -50px;
+ line-height: 16px;
+ font-size: 11px;
+ white-space: normal;
+}
+
+.filter-icon {
+
+}
+
+.filter-tip {
+
+}
+
+.group-by-icon {
+ float: right;
+ margin-right: 5px;
+}
+
+.group-by-tip {
+ margin-top: -300px;
+ margin-left: -475px;
+}
+
+.filter-legend-icon {
+
+}
+
+.filter-legend-tip {
+ margin-left: 10px;
+}
+
+.group_by-legend-icon {
+
+}
+
+.group_by-legend-tip {
+ margin-left: 10px;
+}
\ No newline at end of file
diff --git a/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb
new file mode 100644
index 0000000000..caf05ae379
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting.css.erb
@@ -0,0 +1,575 @@
+/*-- copyright
+ReportingEngine
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++*/
+
+.cost_types {
+ padding-bottom: 3px;
+}
+
+.cost_types a.active {
+ color: #000;
+ font-weight: bold;
+}
+
+.report {
+ text-align: center;
+ border-collapse: collapse;
+ border: solid 1px #ccc !important;
+ width: auto !important;
+}
+
+.report td, .report th {
+ min-width: 90px;
+ white-space: nowrap;
+}
+
+.report td {
+ border: dotted 1px #ddd;
+ color: #666;
+ text-align: center;
+ padding: 0 8px 0 8px;
+ line-height: 2rem;
+ vertical-align: middle;
+
+}
+
+.report tbody th, .report tbody td, .inner {
+ max-width: 300px;
+ white-space: normal !important;
+}
+
+.report td:hover {
+ color: #000;
+ outline: #ccc 1px solid;
+ outline-offset: 1px;
+}
+
+.report td.empty:hover {
+ outline: none;
+}
+
+.report th {
+ border: solid 1px #ccc;
+ background-color: #e3e3e3;
+ text-align: center;
+ font-size: 0.875rem;
+ line-height: 34px;
+ padding: 0 8px 0 8px;
+
+}
+
+.report .odd th.inner {
+ background-color: #e8e8e8;
+}
+
+.report .even th.inner {
+ background-color: #e3e3e3;
+}
+
+.report th.inner {
+ border: solid 1px #ccc;
+ background-color: #efefef;
+ text-align: right;
+}
+
+.report .odd td.inner {
+ background-color: #e8e8e8;
+}
+
+.report .even td.inner {
+ background-color: #e3e3e3;
+}
+
+.report tr.even:hover .inner,
+.report tr.even:hover .bottom,
+.report tr.even:hover .empty,
+.report tr.even:hover .right {
+ background-color: #f5f5c5 !important;
+}
+/* IE7 made me do it! */
+.report tr.odd:hover .inner,
+.report tr.odd:hover .bottom,
+.report tr.odd:hover .empty,
+.report tr.odd:hover .right {
+ background-color: #f5f5c5 !important;
+}
+
+.report .top {
+ border-top-style: solid;
+ border-top-color: #ccc;
+ /* border-top: 2px solid #ccc !important; */
+}
+
+.report .bottom {
+ border-bottom-style: solid;
+ border-bottom-color: #ccc;
+ /* border-bottom: 2px solid #ccc !important; */
+}
+
+.report td.penultimate {
+ border-right-style: solid;
+}
+
+.report thead .inner, .report tfoot .inner {
+ text-align: right;
+ padding-right: 5px;
+}
+
+.report .result {
+ font-size: 120%;
+ text-align: right;
+}
+
+#result-table {
+ margin-top: 10px !important;
+}
+
+.report thead tr:hover .inner, .report tfoot tr:hover .inner {
+ background-color: #efefef;
+}
+
+.report .left {
+ text-align: left !important;
+ padding-left: 5px;
+}
+
+.report .right {
+ text-align: right !important;
+ padding-right: 5px;
+}
+
+/* Details view*/
+
+.detail-report td {
+ text-align: left;
+ vertical-align: top;
+}
+
+#query_form fieldset.header_collapsible.collapsible {
+ padding-bottom: 10px;
+}
+
+/* Overwriting styling for headlines within the query. */
+/* TODO: Font-size seems to be a bit odd. Needs some love. */
+.new_report fieldset h3 {
+ font-size: 1.17em;
+ border: none;
+}
+
+.filter_rem { }
+
+/***** icons *****/
+/*
+ We hide the original content of things with class 'icon' and disply an icon instead.
+ This css class is optimized for usage report breadcrumbs.
+ Hint: The content of the title attribute is shown as a tooltip if the underlying DOM element is an .
+*/
+.breadcrumb_icon {
+ padding: 3px 5px 0 16px;
+ cursor: pointer;
+ background: no-repeat left center;
+ border-style: none;
+ display: inline-block;
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+#query-name-edit-button {
+ margin-left: 16px;
+ font-size: 12px;
+}
+
+.filter {
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.inactive-filter {
+ background-color: #FCE29A !important;
+}
+
+.dependent-filter-label {
+ margin-left: 20px;
+}
+
+.advanced-filters--filter-value {
+ white-space: nowrap;
+}
+
+.filter_radio_option {
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+#add_filter_block {
+ margin-top: 6px;
+}
+
+#add_filter_select {
+ margin-bottom: 10px;
+}
+
+/* ----- group by --- */
+#group_by_area {
+ margin: 5px 0 10px 0;
+}
+
+.in_row {
+ float: left;
+ display: block;
+ list-style: none;
+ border-width: 0px;
+}
+
+.group_by_element {
+ margin-left: -15px;
+ cursor: move;
+}
+
+fieldset.collapsible.header_collapsible legend.in_row {
+ width: inherit;
+ background-image: inherit;
+}
+
+/* have #content here to overwrite line-heigth of other themes through being more specific*/
+#content .group_by_label {
+ margin: 0px;
+ padding: 0px 18px 0 0;
+ min-width: 60px;
+ text-align: center;
+ white-space: nowrap;
+ font-weight: bold;
+ color: #fff;
+ background-color: #767676;
+ height: 35px;
+ line-height: 35px;
+ cursor: move;
+}
+
+.group_by_label.hover {
+ background-color: #3493B3 !important;
+}
+
+.group_by_remove {
+ background-color: #767676;
+ height: 18px;
+ width: 8px;
+ position: absolute;
+ top: 8px;
+ right: 21px;
+ cursor: pointer;
+}
+
+.group_by_remove.hover {
+ background-color: #3493B3 !important;
+}
+
+/* Overwrite normal h3 definition - a h-tag is needed for accessibility purposes */
+h3.reporting_formatting {
+ font-weight:normal;
+ font-size: 13px;
+ margin:0px;
+ padding:0px;
+ color:#333;
+ border:0px;
+}
+
+/* have #content here to overwrite line-heigth of other themes through being more specific*/
+#content .group_by_caption {
+ color: #FFFFFF;
+ background-color: #4B4B4B;
+ font-weight: bold;
+ padding: 0 7px;
+ margin: 0;
+ height: 35px;
+ line-height: 35px;
+ min-width: 55px;
+}
+
+.arrow {
+ height: 0;
+ width: 0;
+ border-style: solid;
+ border-width: 18px 11px 17px 9px;
+}
+
+.arrow_left {
+ border-color: #767676 #767676 #767676 transparent;
+}
+
+.arrow_left.hover {
+ border-color: #3493B3 #3493B3 #3493B3 transparent !important;
+}
+
+.arrow_right {
+ border-color: transparent transparent transparent #767676;
+}
+
+.arrow_right.hover {
+ border-color: transparent transparent transparent #3493B3 !important;
+}
+
+.arrow_group_by_caption {
+ border-color: transparent transparent transparent #4B4B4B;
+}
+
+.drag_container {
+ padding: 0;
+ height: 36px;
+ margin-bottom: 1em;
+ background-color: #EEE;
+}
+
+.drag_container select {
+ margin-right: 3px;
+ float: right;
+ width: auto;
+ padding: 0.2rem 1.5rem;
+}
+
+.drag_target {
+ min-height: 10px;
+}
+
+.drag_container_accept {
+ background-color: #F5F5C5;
+}
+
+/* -- end group-by -- */
+
+td .drill_down, th .drill_down {
+ font-size: 8px;
+ display: block;
+ float: right;
+ font-weight: bold;
+ visibility: hidden;
+}
+
+td:hover .drill_down, th:hover .drill_down {
+ visibility: visible;
+}
+
+/*Buttons*/
+
+.buttons .reporting_button {
+ background-image: url();
+ background-color: #008BD0;
+ border: none;
+ cursor: pointer;
+ margin: 0px;
+ overflow: visible;
+ text-align: center;
+ white-space: nowrap;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ padding: 4px;
+}
+
+.form_controls {
+ margin-top: 6px;
+}
+
+.form_controls .button {
+ margin-bottom: 0;
+}
+
+.form_controls > * {
+ margin: 0 5px 0 5px;
+}
+
+.buttons .reporting_button:hover {
+ background-color: #24B3E7;
+ background-image: url();
+}
+
+.buttons .reporting_button * em {
+ color: white;
+ display: block;
+ font-style: normal;
+ font-weight: bold;
+ line-height: 17px;
+}
+
+.buttons .reporting_button > * {
+ display: inline-block;
+ margin: 0px;
+}
+
+.saved_queries .reporting_button {
+ float: left;
+ margin: 2px;
+}
+
+.buttons .apply {
+ background-image: url();
+ background-color: #008BD0;
+}
+
+.buttons .apply:hover {
+ background-color: #24B3E7;
+ background-image: url();
+}
+
+.buttons .secondary {
+ background-color: #767676;
+ background-image: url();
+}
+
+.buttons .secondary:hover {
+ background-color: #666666;
+ background-image: url();
+}
+
+.buttons .button span em {
+ color: white;
+ display: block;
+ font-style: normal;
+ font-weight: bold;
+ line-height: 17px;
+}
+
+.buttons .button span {
+ display: inline-block;
+ margin: 3px;
+}
+
+
+.buttons .button-icon {
+ padding: 0px 5px 0 16px;
+ cursor: pointer;
+ background: no-repeat left center;
+ border-style: none;
+ display: inline-block;
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+.button .icon-save {
+ background-image: url(<%= asset_path 'reporting_engine/disk.gif' %>);
+}
+
+.button .icon-delete {
+ background-image: url(<%= asset_path 'reporting_engine/delete.gif' %>);
+}
+
+.button .icon-clear {
+ background-image: url(<%= asset_path 'reporting_engine/remove.gif' %>);
+}
+
+div.button_form {
+ /* TODO IE Compatibility! */
+ background-color: white;
+ border: 1px solid gray;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ left: 100px;
+ position: absolute;
+ padding: 1.0rem;
+}
+
+/***** Save and Delete Reports ****/
+#save_as_form, #delete_form {
+ z-index: 999;
+}
+
+#progressbar, .hidden {
+ display: none;
+}
+
+#progressbar_container {
+ width: 300px;
+ height: 16px;
+ border: 1px solid #ccc;
+ padding: 0;
+ margin: 0;
+ position: relative;
+ background-color: #767676;
+ background-repeat: repeat-x;
+}
+
+#progressbar_container div {
+ background-color:#fff;
+}
+
+/***** Ajax indicator ******/
+#ajax-indicator {
+ font-family: Verdana, sans-serif;
+ position: absolute; /* fixed not supported by IE */
+ background-color:#eee;
+ border: 1px solid #bbb;
+ top:35%;
+ left:40%;
+ width:20%;
+ font-weight:bold;
+ text-align:center;
+ padding:0.6em;
+ z-index:100;
+ filter:alpha(opacity=50);
+ opacity: 0.5;
+ line-height: 10px;
+ font-size: 18px;
+}
+
+html>body #ajax-indicator { position: fixed; }
+
+#ajax-indicator span {
+ background-position: 0% 40%;
+ background-repeat: no-repeat;
+ background-image: url(<%= asset_path 'reporting_engine/loading.gif' %>);
+ padding-left: 26px;
+ vertical-align: bottom;
+}
+
+/* Calendar Fixes */
+div.calendar /* let calendar expand properly */
+{
+ font-size: medium;
+ line-height: normal;
+}
+
+div.calendar table div { /* make sure the nested divs are large enough, too */
+ font-size: medium;
+ line-height: normal;
+}
+
+.calendar .combo .label, .calendar .combo .label-IEfix { /* set the proper size for the combo boxes with months and years */
+ font-size: 10px;
+ line-height: normal;
+}
+
+.calendar tbody .day { /* avoid jitter during quick mouse-overs over days */
+ border: 1px dotted transparent;
+ padding: 1px 3px 1px 1px;
+}
+
+/* Accessibility specific styles */
+fieldset#filters table td > label.hidden-for-sighted, .hidden-for-sighted {
+ position:absolute;
+ left:-10000px;
+ top:auto;
+ width:1px;
+ height:1px;
+ overflow:hidden;
+}
+
+.advanced-filters--filter-value.-binary {
+ display: flex;
+}
+
diff --git a/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css
new file mode 100644
index 0000000000..4c8fa266a5
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/assets/stylesheets/reporting_engine/reporting_engine.css
@@ -0,0 +1,24 @@
+/*-- copyright
+ReportingEngine
+
+Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+++*/
+
+/* *
+ *= require reporting_engine/help
+ *= require reporting_engine/reporting
+ */
\ No newline at end of file
diff --git a/vendored-plugins/reporting_engine/lib/engine.rb b/vendored-plugins/reporting_engine/lib/engine.rb
new file mode 100644
index 0000000000..47933bf253
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/engine.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Engine
+ ##
+ # Subclass of Report to be used for constant lookup and such.
+ # It is considered public API to override this method i.e. in Tests.
+ #
+ # @return [Class] subclass
+ def engine
+ return @engine if @engine
+ if is_a? Module
+ @engine = Object.const_get(name[/^[^:]+/] || :Report)
+ elsif respond_to? :parent and parent.respond_to? :engine
+ parent.engine
+ else
+ self.class.engine
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report.rb b/vendored-plugins/reporting_engine/lib/report.rb
new file mode 100644
index 0000000000..216c89c1ff
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report.rb
@@ -0,0 +1,213 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report < ActiveRecord::Base
+ extend Forwardable
+ include Enumerable
+ include Engine
+
+ belongs_to :user
+ belongs_to :project
+
+ before_save :serialize
+ serialize :serialized, Hash
+
+ self.abstract_class = true # lets have subclasses have their own SQL tables
+
+ def self.accepted_properties
+ @@accepted_properties ||= []
+ end
+
+ def self.reporting_connection
+ connection
+ end
+
+ def self.chain_initializer
+ @chain_initializer ||= []
+ end
+
+ def self.deserialize(hash, object = new)
+ object.tap do |q|
+ hash[:filters].each { |name, opts| q.filter(name, opts) }
+ hash[:group_bys].each { |name, opts| q.group_by(name, opts) }
+ end
+ end
+
+ def serialize
+ # have to take the reverse group_bys to retain the original order when deserializing
+ self.serialized = { filters: (filters.map(&:serialize).reject(&:nil?).sort { |a, b| a.first <=> b.first }),
+ group_bys: group_bys.map(&:serialize).reject(&:nil?).reverse }
+ end
+
+ def deserialize
+ unless @chain
+ hash = serialized || serialize
+ self.class.deserialize(hash, self)
+ else
+ raise ArgumentError, 'Cannot deserialize a report which already has a chain'
+ end
+ end
+
+ # Convenience method to generate a params hash readable by Controller#determine_settings
+ def to_params
+ params = {}
+ filters.select { |f| f.class.display? }.each do |f|
+ filter_name = f.class.underscore_name
+ params[:fields] << filter_name
+ params[:operators].merge! filter_name => f.operator.to_s
+ params[:values].merge! filter_name => f.values
+ end
+ group_bys.each do |g|
+ params[:groups] ||= { rows: [], columns: [] }
+ params[:groups][g.row? ? :rows : :columns] << g.class.underscore_name
+ end
+ params
+ end
+
+ ##
+ # Migrates this report to look like the given report.
+ # This may be used to alter report properties without
+ # creating a new report in a database.
+ def migrate(report)
+ [:@chain, :@query, :@transformer, :@walker, :@table, :@depths, :@chain_initializer].each do |inst_var|
+ instance_variable_set inst_var, (report.instance_variable_get inst_var)
+ end
+ end
+
+ def available_filters
+ self.class::Filter.all
+ end
+
+ def transformer
+ @transformer ||= self.class::Transformer.new self
+ end
+
+ def walker
+ @walker ||= self.class::Walker.new self
+ end
+
+ def add_chain(type, name, options)
+ chain type.const_get(name.to_s.camelcase), options
+ @transformer, @table, @depths, @walker = nil, nil, nil, nil
+ self
+ end
+
+ def chain(klass = nil, options = {})
+ build_new_chain unless @chain
+ if klass
+ @chain = klass.new @chain, options
+ @chain.engine = self.class
+ end
+ @chain = @chain.parent until @chain.top?
+ @chain
+ end
+
+ def build_new_chain
+ # FIXME: is there a better way to load all filter and groups?
+ self.class::Filter.all && self.class::GroupBy.all
+
+ minimal_chain!
+ self.class.chain_initializer.each { |block| block.call self }
+ self
+ end
+
+ def filter(name, options = {})
+ add_chain self.class::Filter, name, options
+ end
+
+ def group_by(name, options = {})
+ add_chain self.class::GroupBy, name, options.reverse_merge(type: :column)
+ end
+
+ def column(name, options = {})
+ group_by name, options.merge(type: :column)
+ end
+
+ def row(name, options = {})
+ group_by name, options.merge(type: :row)
+ end
+
+ def table
+ @table = self.class::Table.new(self)
+ end
+
+ def group_bys(type = nil)
+ chain.select { |c| c.group_by? && (type.nil? || c.type == type) }
+ end
+
+ def filters
+ chain.select(&:filter?)
+ end
+
+ def depth_of(name)
+ @depths ||= {}
+ @depths[name] ||= chain.inject(0) { |sum, child| child.type == name ? sum + 1 : sum }
+ end
+
+ def_delegators :transformer, :column_first, :row_first
+ def_delegators :chain, :empty_chain, :top, :bottom, :chain_collect, :sql_statement, :all_group_fields, :child, :clear, :result
+ def_delegators :result, :each_direct_result, :recursive_each, :recursive_each_with_level, :each, :each_row, :count,
+ :units, :final_number
+ def_delegators :table, :row_index, :colum_index
+
+ def to_a
+ chain.to_a
+ end
+
+ def to_s
+ chain.to_s
+ end
+
+ def size
+ size = 0
+ recursive_each { |r| size += r.size }
+ size
+ end
+
+ def cache_key
+ deserialize unless @chain
+ parts = [self.class.table_name.sub('_reports', '')]
+ parts.concat [filters.sort, group_bys].map { |l| l.map(&:cache_key).join(' ') }
+ parts.join '/'
+ end
+
+ def self.engine
+ self
+ end
+
+ def minimal_chain!
+ @chain = self.class::Filter::NoFilter.new
+ end
+
+ def public!
+ self.is_public = true
+ end
+
+ def public?
+ is_public
+ end
+
+ def private!
+ self.is_public = false
+ end
+
+ def private?
+ !public?
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/chainable.rb b/vendored-plugins/reporting_engine/lib/report/chainable.rb
new file mode 100644
index 0000000000..9cfbab19e4
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/chainable.rb
@@ -0,0 +1,346 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# Provides convinience layer and logic shared between GroupBy::Base and Filter::Base.
+# Implements a double linked list (FIXME: is that the correct term?).
+class Report < ActiveRecord::Base
+ class Chainable
+ include Enumerable
+ include Report::QueryUtils
+ extend Report::InheritedAttribute
+ extend Forwardable
+
+ # this attr. should point to a symbol useable for translations
+ inherited_attribute :applies_for, default: :label_cost_entry_attributes
+ def_delegators :'self.class', :table_joins, :table_name, :field, :display?, :help_text, :underscore_name
+
+ def self.accepts_property(*list)
+ engine.accepted_properties.push(*list.map(&:to_s))
+ end
+
+ def self.chain_list(*list)
+ options = list.extract_options!
+ options[:list] = true
+ list << options
+ inherited_attribute(*list)
+ end
+
+ def self.base?
+ superclass == engine::Chainable or self == engine::Chainable or
+ superclass == Chainable or self == Chainable or
+ self == engine::Filter::Base or self == engine::GroupBy::Base
+ end
+
+ def self.base
+ return self if base?
+ superclass.base
+ end
+
+ def self.from_base(&block)
+ base.instance_eval(&block)
+ end
+
+ def self.available
+ from_base { @available ||= [] }
+ end
+
+ def self.register(label)
+ available << klass
+ set_inherited_attribute 'label', label
+ end
+
+ def self.table_joins
+ (@table_joins ||= []).clone
+ end
+
+ def self.table_from(value)
+ return value.table_name if value.respond_to? :table_name
+ return value unless value.respond_to? :to_ary or value.respond_to? :to_hash
+ table_from value.to_a.first
+ end
+
+ def self.join_table(*args)
+ @last_table = table_from(args.last)
+ (@table_joins ||= []) << args
+ end
+
+ def self.underscore_name
+ name.demodulize.underscore
+ end
+
+ def self.put_sql_table_names(table_prefix_placement = {})
+ @table_prefix_placement ||= {}
+ @table_prefix_placement.merge! table_prefix_placement
+ @table_prefix_placement
+ end
+
+ ##
+ # The given block is called when a new chain is created for a report.
+ # The query will be given to the block as a parameter.
+ # Example:
+ # initialize_query_with { |query| query.filter Report::Filter::City, :operators => '=', :values => 'Berlin, da great City' }
+ def self.initialize_query_with(&block)
+ engine.chain_initializer.push block
+ end
+
+ def self.cache_key
+ @cache_key ||= underscore_name
+ end
+
+ inherited_attribute :properties, list: true
+
+ def self.label
+ 'Translation needed'
+ end
+
+ class << self
+ alias inherited_attributes inherited_attribute
+ alias accepts_properties accepts_property
+ end
+
+ attr_accessor :parent, :child, :type
+ accepts_property :type
+
+ def each(&block)
+ yield self
+ child.try(:each, &block)
+ end
+
+ def row?
+ type == :row
+ end
+
+ def column?
+ type == :column
+ end
+
+ def group_by?
+ !filter?
+ end
+
+ def to_a
+ [to_hash].tap { |a| a.unshift(*child.to_a) unless bottom? }
+ end
+
+ def top
+ return self if top?
+ parent.top
+ end
+
+ def top?
+ parent.nil?
+ end
+
+ def bottom?
+ child.nil?
+ end
+
+ def bottom
+ return self if bottom?
+ child.bottom
+ end
+
+ def initialize(child = nil, options = {})
+ @options = options
+ options.each do |key, value|
+ unless self.class.extra_options.include? key
+ raise ArgumentError, "may not set #{key}" unless engine.accepted_properties.include? key.to_s
+ send "#{key}=", value
+ end
+ end
+ self.child, child.parent = child, self if child
+ move_down until correct_position?
+ clear
+ end
+
+ def to_a
+ cached :compute_to_a
+ end
+
+ def compute_to_a
+ [[self.class.field, @options], *child.try(:to_a)].compact
+ end
+
+ def to_s
+ URI.escape to_a.map(&:join).join(',')
+ end
+
+ def serialize
+ [self.class.to_s.demodulize, @options]
+ end
+
+ def move_down
+ reorder parent, child, self, child.child
+ end
+
+ ##
+ # Reorder given elements of a doubly linked list to follow the lists order.
+ # Don't use this for evil. Assumes there are no elements inbetween, does
+ # not touch the first element's parent and the last element's child.
+ # Does not touch elements not part of the list.
+ #
+ # @param [Array] *list Part of the linked list
+ def reorder(*list)
+ list.each_with_index do |entry, index|
+ next_entry = list[index + 1]
+ entry.try(:child=, next_entry) if index < list.size - 1
+ next_entry.try(:parent=, entry)
+ end
+ end
+
+ def chain_collect(name, *args, &block)
+ top.subchain_collect(name, *args, &block)
+ end
+
+ # See #chain_collect
+ def subchain_collect(name, *args, &block)
+ subchain = child.subchain_collect(name, *args, &block) unless bottom?
+ [* send(name, *args, &block)].push(*subchain).compact.uniq
+ end
+
+ # overwrite in subclass to maintain constisten state
+ # ie automatically turning
+ # FilterFoo.new(GroupByFoo.new(FilterBar.new))
+ # into
+ # GroupByFoo.new(FilterFoo.new(FilterBar.new))
+ # Returning false will make the
+ def correct_position?
+ true
+ end
+
+ def clear
+ @cached = nil
+ child.try :clear
+ end
+
+ def result
+ cached(:compute_result)
+ end
+
+ def compute_result
+ engine::Result.new engine.reporting_connection.select_all(sql_statement.to_s), {}, type
+ end
+
+ def cached(*args)
+ @cached ||= {}
+ @cached[args] ||= send(*args)
+ end
+
+ def sql_statement
+ raise "should not get here (#{inspect})" if bottom?
+ child.cached(:sql_statement).tap do |q|
+ chain_collect(:table_joins).each { |args| q.join(*args) } if responsible_for_sql?
+ end
+ end
+
+ inherited_attribute :db_field
+ def self.field
+ db_field || (name[/[^:]+$/] || name).to_s.underscore
+ end
+
+ def display?
+ self.class.display?
+ end
+
+ inherited_attribute :display, default: true
+ def self.display!
+ display true
+ end
+
+ def self.display?
+ !!display
+ end
+
+ def self.dont_display!
+ display false
+ not_selectable!
+ end
+
+ inherited_attribute :selectable, default: true
+ def self.selectable!
+ selectable true
+ end
+
+ def self.selectable?
+ !!selectable
+ end
+
+ def self.not_selectable!
+ selectable false
+ end
+
+ # Extra options this chainable accepts that are not defined in accepted_properties
+ def self.extra_options(*symbols)
+ @extra_option ||= []
+ @extra_option += symbols
+ end
+
+ # This chainable type can only ever occur once in a chain
+ def self.singleton
+ class << self
+ def new(chain = nil, options = {})
+ return chain if chain and chain.map(&:class).include? self
+ super
+ end
+ end
+ end
+
+ def self.last_table
+ @last_table ||= engine::Filter::NoFilter.table_name
+ end
+
+ def self.table_name(value = nil)
+ @table_name = table_name_for(value) if value
+ @table_name || last_table
+ end
+
+ def with_table(fields)
+ fields.map do |f|
+ place_field_name = self.class.put_sql_table_names[f] || self.class.put_sql_table_names[f].nil?
+ place_field_name ? (field_name_for f, self) : f
+ end
+ end
+
+ def mapping
+ self.class.method(:mapping).to_proc
+ end
+
+ def self.mapping(value)
+ value.to_s
+ end
+
+ def self.mapping_for(field)
+ @field_map ||= (engine::Filter.all + engine.GroupBy.all).inject(Hash.new { |h, k| h[k] = [] }) do |hash, cbl|
+ hash[cbl.field] << cbl.mapping
+ end
+ @field_map[field]
+ end
+
+ ##
+ # Sets a help text to be displayed for this kind of Chainable.
+ def self.help_text=(sym)
+ @help_text = sym
+ end
+
+ def self.help_text(sym = nil)
+ @help_text = sym if sym
+ @help_text
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/controller.rb b/vendored-plugins/reporting_engine/lib/report/controller.rb
new file mode 100644
index 0000000000..934122f6da
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/controller.rb
@@ -0,0 +1,358 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'json'
+
+module Report::Controller
+ def self.included(base)
+ base.class_eval do
+ attr_accessor :report_engine
+ helper_method :current_user
+ helper_method :allowed_to?
+
+ include ReportingHelper
+ helper ReportingHelper
+ helper { def engine; @report_engine; end }
+
+ before_filter :determine_engine
+ before_filter :prepare_query, only: [:index, :create]
+ before_filter :find_optional_report, only: [:index, :show, :update, :destroy, :rename]
+ before_filter :possibly_only_narrow_values
+ before_filter { @no_progress = no_progress? }
+ end
+ end
+
+ def index
+ table
+ end
+
+ ##
+ # Render the report. Renders either the complete index or the table only
+ def table
+ if set_filter? && request.xhr?
+ if no_progress?
+ table_without_progress_info
+ else
+ table_with_progress_info
+ end
+ end
+ end
+
+ def table_without_progress_info
+ # stream result to client
+ self.response_body = render_widget(Widget::Table, @query)
+ end
+
+ def table_with_progress_info
+ render text: render_widget(Widget::Table::Progressbar, @query), layout: !request.xhr? and return
+ end
+
+ ##
+ # Create a new saved query. Returns the redirect url to an XHR or redirects directly
+ def create
+ @query.name = params[:query_name].present? ? params[:query_name] : ::I18n.t(:label_default)
+ @query.public! if make_query_public?
+ @query.send("#{user_key}=", current_user.id)
+ @query.save!
+ if request.xhr? # Update via AJAX - return url for redirect
+ render text: url_for(action: 'show', id: @query.id)
+ else # Redirect to the new record
+ redirect_to action: 'show', id: @query.id
+ end
+ end
+
+ ##
+ # Show a saved record, if found. Raises RecordNotFound if the specified query
+ # at :id does not exist
+ def show
+ if @query
+ store_query(@query)
+ table
+ render action: 'index' unless performed?
+ else
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+
+ ##
+ # Delete a saved record, if found. Redirects to index on success, raises a
+ # RecordNotFound if the query at :id does not exist
+ def destroy
+ if @query
+ @query.destroy if allowed_to? :delete, @query
+ else
+ raise ActiveRecord::RecordNotFound
+ end
+ redirect_to action: 'index', default: 1
+ end
+
+ ##
+ # Update a record with new query parameters and save it. Redirects to the
+ # specified record or renders the updated table on XHR
+ def update
+ if params[:set_filter].to_i == 1 # save
+ old_query = @query
+ prepare_query
+ old_query.migrate(@query)
+ old_query.save!
+ @query = old_query
+ end
+ if request.xhr?
+ table
+ else
+ redirect_to action: 'show', id: @query.id
+ end
+ end
+
+ ##
+ # Rename a record and update its publicity. Redirects to the updated record or
+ # renders the updated name on XHR
+ def rename
+ @query.name = params[:query_name]
+ @query.public! if make_query_public?
+ @query.save!
+ store_query(@query)
+ unless request.xhr?
+ redirect_to action: 'show', id: @query.id
+ else
+ render text: @query.name
+ end
+ end
+
+ ##
+ # Determine the available values for the specified filter and return them as
+ # json, if that was requested. This will be executed INSTEAD of the actual action
+ def possibly_only_narrow_values
+ if params[:narrow_values] == '1'
+ sources = params[:sources]
+ dependent = params[:dependent]
+
+ query = report_engine.new
+ sources.each do |dependency|
+ query.filter(dependency.to_sym,
+ operator: params[:operators][dependency],
+ values: params[:values][dependency])
+ end
+ query.column(dependent)
+ values = [[::I18n.t(:label_inactive), '<>']] + query.result.map { |r| r.fields[query.group_bys.first.field] }
+ # replace null-values with corresponding placeholder
+ values = values.map { |value| value.nil? ? [::I18n.t(:label_none), '<>'] : value }
+ # try to find corresponding labels to the given values
+ values = values.map do |value|
+ filter = report_engine::Filter.const_get(dependent.camelcase.to_sym)
+ filter_value = filter.label_for_value value
+ if filter_value && filter_value.first.is_a?(Symbol)
+ [::I18n.t(filter_value.first), filter_value.second]
+ elsif filter_value && filter_value.first.is_a?(String)
+ [filter_value.first, filter_value.second]
+ else
+ value
+ end
+ end
+ render json: values.to_json
+ end
+ end
+
+ ##
+ # Determine the requested engine by constantizing from the :engine parameter
+ # Sets @report_engine and @title based on that, and makes the engine available
+ # to views and widgets via the #engine method.
+ # Raises RecordNotFound on failure
+ def determine_engine
+ @report_engine = params[:engine].constantize
+ @title = "label_#{@report_engine.name.underscore}"
+ rescue NameError
+ raise ActiveRecord::RecordNotFound, 'No engine found - override #determine_engine'
+ end
+
+ ##
+ # Determines if the request contains filters to set
+ def set_filter? # FIXME: rename to set_query?
+ params[:set_filter].to_i == 1
+ end
+
+ ##
+ # Determines if the requested table should be rendered with a progressbar
+ def no_progress?
+ !!params[:immediately]
+ end
+
+ ##
+ # Return the active filters
+ def filter_params
+ filters = http_filter_parameters if set_filter?
+ filters ||= session[report_engine.name.underscore.to_sym].try(:[], :filters)
+ filters ||= default_filter_parameters
+ end
+
+ ##
+ # Return the active group bys
+ def group_params
+ groups = http_group_parameters if set_filter?
+ groups ||= session[report_engine.name.underscore.to_sym].try(:[], :groups)
+ groups ||= default_group_parameters
+ end
+
+ ##
+ # Extract active filters from the http params
+ def http_filter_parameters
+ params[:fields] ||= []
+ (params[:fields].reject(&:empty?) || []).inject(operators: {}, values: {}) do |hash, field|
+ hash[:operators][field.to_sym] = params[:operators][field]
+ hash[:values][field.to_sym] = params[:values][field]
+ hash
+ end
+ end
+
+ ##
+ # Extract active group bys from the http params
+ def http_group_parameters
+ if params[:groups]
+ rows = params[:groups]['rows']
+ columns = params[:groups]['columns']
+ end
+ { rows: (rows || []), columns: (columns || []) }
+ end
+
+ ##
+ # Set a default query to cut down initial load time
+ def default_filter_parameters
+ { operators: {}, values: {} }
+ end
+
+ ##
+ # Set a default query to cut down initial load time
+ def default_group_parameters
+ { columns: [:sector_id], rows: [:country_id] }
+ end
+
+ ##
+ # Determines if the query settings should be reset
+ def force_default?
+ params[:default].to_i == 1
+ end
+
+ ##
+ # Prepare the query from the request
+ def prepare_query
+ determine_settings
+ @query = build_query(session[report_engine.name.underscore.to_sym][:filters],
+ session[report_engine.name.underscore.to_sym][:groups])
+ end
+
+ ##
+ # Determine the query settings the current request and save it to
+ # the session.
+ def determine_settings
+ if force_default?
+ filters = default_filter_parameters
+ groups = default_group_parameters
+ session[report_engine.name.underscore.to_sym].try :delete, :name
+ else
+ filters = filter_params
+ groups = group_params
+ end
+ cookie = session[report_engine.name.underscore.to_sym] || {}
+ session[report_engine.name.underscore.to_sym] = cookie.merge(filters: filters, groups: groups)
+ end
+
+ ##
+ # Build the query from the passed session hash
+ def build_query(filters, groups = {})
+ query = report_engine.new
+ query.tap do |q|
+ filters[:operators].each do |filter, operator|
+ unless filters[:values][filter] == ['<>']
+ values = Array(filters[:values][filter]).map { |v| v == '<>' ? nil : v }
+ q.filter(filter.to_sym,
+ operator: operator,
+ values: values)
+ end
+ end
+ end
+ groups[:rows].try(:reverse_each) { |r| query.row(r) }
+ groups[:columns].try(:reverse_each) { |c| query.column(c) }
+ query
+ end
+
+ ##
+ # Store query in the session
+ def store_query(_query)
+ cookie = {}
+ cookie[:groups] = @query.group_bys.inject({}) do |h, group|
+ ((h[:"#{group.type}s"] ||= []) << group.underscore_name.to_sym) && h
+ end
+ cookie[:filters] = @query.filters.inject(operators: {}, values: {}) do |h, filter|
+ h[:operators][filter.underscore_name.to_sym] = filter.operator.to_s
+ h[:values][filter.underscore_name.to_sym] = filter.values
+ h
+ end
+ cookie[:name] = @query.name if @query.name
+ session[report_engine.name.underscore.to_sym] = cookie
+ end
+
+ ##
+ # Override in subclass if user key
+ def user_key
+ 'user_id'
+ end
+
+ ##
+ # Override in subclass if you like
+ def is_public_sql(val = true)
+ "(is_public = #{val ? report_engine.reporting_connection.quoted_true : report_engine.reporting_connection.quoted_false})"
+ end
+
+ ##
+ # Abstract: Implementation required in application
+ def allowed_to?(_action, _subject, _user = current_user)
+ raise NotImplementedError, "The #{self.class} should have implemented #allowed_to?(action, subject, user)"
+ end
+
+ def make_query_public?
+ !!params[:query_is_public]
+ end
+
+ # renders option tags for each available value for a single filter
+ def available_values
+ if name = params[:filter_name]
+ f_cls = report_engine::Filter.const_get(name.to_s.camelcase)
+ filter = f_cls.new.tap do |f|
+ f.values = JSON.parse(params[:values].gsub("'", '"')) if params[:values].present? and params[:values]
+ end
+ render_widget Widget::Filters::Option, filter, to: canvas = ''
+ render text: canvas, layout: !request.xhr?
+ end
+ end
+
+ ##
+ # Find a report if :id was passed as parameter.
+ # Raises RecordNotFound if an invalid :id was passed.
+ #
+ # @param query An optional query added to the disjunction qualifiying reports to be returned.
+ def find_optional_report(query = '1=0')
+ if params[:id]
+ @query = report_engine
+ .where(["#{is_public_sql} OR (#{user_key} = ?) OR (#{query})", current_user.id])
+ .find(params[:id].to_i)
+ @query.deserialize if @query
+ end
+ rescue ActiveRecord::RecordNotFound
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/filter.rb b/vendored-plugins/reporting_engine/lib/report/filter.rb
new file mode 100644
index 0000000000..113f7a0202
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/filter.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'set'
+
+class Report::Filter
+ def self.all
+ @all ||= Set[]
+ end
+
+ def self.reset!
+ @all = nil
+ end
+
+ def self.all_grouped
+ all.group_by(&:applies_for).to_a.sort { |a, b| a.first.to_s <=> b.first.to_s }
+ end
+
+ def self.from_hash
+ raise NotImplementedError
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/filter/base.rb b/vendored-plugins/reporting_engine/lib/report/filter/base.rb
new file mode 100644
index 0000000000..05b5b13004
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/filter/base.rb
@@ -0,0 +1,230 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Filter
+ class Base < Report::Chainable
+ include Report::QueryUtils
+ engine::Operator.load
+
+ inherited_attribute :available_operators,
+ list: true, map: :to_operator,
+ uniq: true
+ inherited_attribute :default_operator, map: :to_operator
+
+ accepts_property :values, :value, :operator
+
+ mattr_accessor :skip_inherited_operators
+ self.skip_inherited_operators = [:time_operators, 'y', 'n']
+
+ attr_accessor :values
+
+ def cache_key
+ self.class.cache_key + operator.to_s + Array(values).join(',')
+ end
+
+ ##
+ # A Filter is 'heavy' if it possibly returns a _hughe_ number of available_values.
+ # In that case the UI-guys should think twice about displaying all the values.
+ def self.heavy?
+ false
+ end
+
+ # Indicates whether this Filter is a multiple choice filter,
+ # meaning that the user must select a value of a given set of choices.
+ def self.is_multiple_choice?
+ false
+ end
+
+ ##
+ # A Filter may have depentent filters. See the following example:
+ # Filter::Project.dependents --> [Filter::IssueId]
+ # This could result in a UI where, if the Project-Filter was selected,
+ # the IssueId-filter automatically shows up.
+ # Arguments:
+ # - any subclass of Reporting::Filter::Base which shall be the dependent filter
+ # - OR multiple Filters if there are multiple possible dependents and you
+ # want the application-js to decide which dependent to follow
+ def self.dependent(*args)
+ @dependents ||= []
+ @dependents += args unless args.empty?
+ @dependents
+ end
+ class << self
+ alias :dependents :dependent
+ end
+
+ # need this for sort
+ def <=>(other)
+ self.class.underscore_name <=> other.class.underscore_name
+ end
+
+ def self.has_dependent?
+ !dependents.empty?
+ end
+
+ ##
+ # Returns an array of filters of which this filter is a dependent
+ def self.dependent_from
+ engine::Filter.all.select { |f| f.dependents.include? self }
+ end
+
+ ##
+ # Returns true/false depending of wether any filter has this filter a a dependent
+ def self.is_dependent?
+ !dependent_from.empty?
+ end
+
+ def self.cached(*args)
+ @cached ||= {}
+ @cached[args] ||= send(*args)
+ end
+
+ ##
+ # all_dependents computes the depentends of this filter and recursively
+ # all_dependents of this class' dependents.
+ def self.all_dependents
+ cached(:compute_all_dependents)
+ end
+
+ def self.compute_all_dependents(starting_from = nil)
+ starting_from ||= dependents
+ starting_from.inject([]) do |list, dependent|
+ list + Array(dependent) + dependent.all_dependents
+ end
+ end
+
+ def value=(val)
+ self.values = [val]
+ end
+
+ ##
+ # Always empty. You may include additional_operators as a filter module.
+ # This is here for the case you don't.
+ def additional_operators
+ []
+ end
+
+ def self.use(*names)
+ operators = []
+ names.each do |name|
+ dont_inherit :available_operators if skip_inherited_operators.include? name
+ case name
+ when String, engine::Operator then operators << name.to_operator
+ when Symbol then operators.push(*engine::Operator.send(name))
+ else fail "dunno what to do with #{name.inspect}"
+ end
+ end
+ available_operators *operators
+ end
+
+ use :default_operators
+
+ def self.new(*args, &block) # :nodoc:
+ # this class is abstract. instances are only allowed from child classes
+ raise "#{name} is an abstract class" if base?
+ super
+ end
+
+ def self.inherited(klass)
+ if base?
+ self.dont_display!
+ klass.display!
+ end
+ super
+ end
+
+ ##
+ # Returns an array of [:label_of_value, value]-kind arrays, containing
+ # valid id-label combinations of possible filter values
+ def self.available_values(_params = {})
+ []
+ end
+
+ ##
+ # Returns a [:label_of_value, value]-kind array (as in self.vailable_values)
+ # for the given value
+ def self.label_for_value(value)
+ available_values(reverse_search: true).find { |v| v.second == value || v.second.to_s == value }
+ end
+
+ def correct_position?
+ child.nil? or child.filter?
+ end
+
+ def from_for(scope)
+ super + self.class.table_joins
+ end
+
+ def filter?
+ true
+ end
+
+ def valid?
+ @operator ? @operator.validate(values) : true
+ end
+
+ def errors
+ @operator ? @operator.errors : []
+ end
+
+ def group_by_fields
+ []
+ end
+
+ def initialize(child = nil, options = {})
+ @values = []
+ super
+ end
+
+ def might_be_responsible
+ parent
+ end
+
+ def operator
+ (@operator || self.class.default_operator || engine::Operator.default_operator).to_operator
+ end
+
+ def operator=(value)
+ @operator = value.to_operator.tap do |o|
+ unless available_operators.include?(o) || additional_operators.include?(o)
+ raise ArgumentError, "#{o.inspect} not supported by #{inspect}."
+ end
+ end
+ end
+
+ def responsible_for_sql?
+ top?
+ end
+
+ def to_hash
+ raise NotImplementedError
+ end
+
+ def sql_statement
+ super.tap do |query|
+ arity = operator.arity
+ values = [*self.values].compact
+ # if there is just the nil it might be actually intendet to be there
+ values.unshift nil if Array(self.values).size == 1 && Array(self.values).first.nil?
+ values = values[0, arity] if values and arity >= 0 and arity != values.size
+ operator.modify(query, field, *values) unless field.empty?
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/filter/multi_choice.rb b/vendored-plugins/reporting_engine/lib/report/filter/multi_choice.rb
new file mode 100644
index 0000000000..a821f3a09f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/filter/multi_choice.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Filter
+ class MultiChoice < Base
+ dont_inherit :available_operators
+ use '='
+
+ def self.is_multiple_choice?
+ true
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/filter/no_filter.rb b/vendored-plugins/reporting_engine/lib/report/filter/no_filter.rb
new file mode 100644
index 0000000000..52c9a4b380
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/filter/no_filter.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Filter::NoFilter < Report::Filter::Base
+ table_name 'entries'
+ dont_display!
+ singleton
+
+ def sql_statement
+ raise NotImplementedError, "My subclass should have overwritten 'sql_statement'"
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/group_by.rb b/vendored-plugins/reporting_engine/lib/report/group_by.rb
new file mode 100644
index 0000000000..e044473613
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/group_by.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'set'
+
+class Report::GroupBy
+ include Report::QueryUtils
+
+ def self.all
+ Set[engine::GroupBy::SingletonValue]
+ end
+
+ def self.reset!
+ @all = nil
+ end
+
+ def self.all_grouped
+ all.group_by(&:applies_for).to_a.sort { |a, b| a.first.to_s <=> b.first.to_s }
+ end
+
+ def self.from_hash
+ raise NotImplementedError
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/group_by/base.rb b/vendored-plugins/reporting_engine/lib/report/group_by/base.rb
new file mode 100644
index 0000000000..4b4e632dd0
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/group_by/base.rb
@@ -0,0 +1,108 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::GroupBy
+ class Base < Report::Chainable
+ include Report::QueryUtils
+
+ inherited_attributes :group_fields, list: true, merge: false
+
+ def correct_position?
+ type == :row or !child.is_a?(engine::GroupBy::Base) or child.type == :column
+ end
+
+ def filter?
+ false
+ end
+
+ def sql_aggregation?
+ child.filter?
+ end
+
+ def cache_key
+ self.class.cache_key + type.to_s[0, 1]
+ end
+
+ ##
+ # @param [FalseClass, TrueClass] prefix Whether or not add a table prefix the field names
+ # @return [Array] List of group by fields corresponding to self and all parents'
+ def all_group_fields(prefix = true)
+ @all_group_fields ||= []
+ @all_group_fields[prefix ? 0 : 1] ||= begin
+ fields = group_fields.reject { |c| c.blank? or c == 'base' }
+ (parent ? parent.all_group_fields(prefix) : []) + (prefix ? with_table(fields) : fields)
+ end.uniq
+ end
+
+ def self.select_fields(*fields)
+ unless fields.empty?
+ @select_fields ||= []
+ @select_fields += fields
+ end
+ @select_fields
+ end
+
+ def select_fields
+ # + (parent ? parent.select_fields : [])
+ self.class.select_fields ? self.class.select_fields : group_fields
+ end
+
+ ##
+ # @param [FalseClass, TrueClass] prefix Whether or not add a table prefix the field names
+ # @return [Array] List of select fields corresponding to self and all parents'
+ def all_select_fields(prefix = true)
+ @all_select_fields ||= []
+ @all_select_fields[prefix ? 0 : 1] ||= begin
+ fields = select_fields.reject { |c| c.blank? or c == 'base' }
+ (parent ? parent.all_select_fields(prefix) : []) + (prefix ? with_table(fields) : fields)
+ end.uniq
+ end
+
+ def clear
+ @all_group_fields = @all_select_fields = nil
+ super
+ end
+
+ def aggregation_mixin
+ sql_aggregation? ? engine::GroupBy::SqlAggregation : engine::GroupBy::RubyAggregation
+ end
+
+ def initialize(child = nil, optios = {})
+ super
+ extend aggregation_mixin
+ group_fields field
+ end
+
+ def result
+ super
+ end
+
+ def compute_result
+ super.tap do |r|
+ r.type = type
+ r.important_fields = group_fields
+ end
+ end
+
+ def define_group(sql)
+ sql.select all_select_fields
+ sql.group_by all_group_fields(true)
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/group_by/ruby_aggregation.rb b/vendored-plugins/reporting_engine/lib/report/group_by/ruby_aggregation.rb
new file mode 100644
index 0000000000..9b834f535a
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/group_by/ruby_aggregation.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::GroupBy
+ module RubyAggregation
+ def responsible_for_sql?
+ false
+ end
+
+ ##
+ # @return [Report::Result] aggregation
+ def compute_result
+ child.result.grouped_by(all_group_fields(false), type, group_fields)
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/group_by/singleton_value.rb b/vendored-plugins/reporting_engine/lib/report/group_by/singleton_value.rb
new file mode 100644
index 0000000000..d2d016273d
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/group_by/singleton_value.rb
@@ -0,0 +1,32 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::GroupBy
+ class SingletonValue < Base
+ dont_display!
+
+ put_sql_table_names 'singleton_value' => false
+ select_fields '1 as singleton_value'
+
+ def define_group(sql)
+ sql.select '1 as singleton_value'
+ sql.group_by 'singleton_value'
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/group_by/sql_aggregation.rb b/vendored-plugins/reporting_engine/lib/report/group_by/sql_aggregation.rb
new file mode 100644
index 0000000000..6a203c80df
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/group_by/sql_aggregation.rb
@@ -0,0 +1,37 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::GroupBy
+ module SqlAggregation
+ def responsible_for_sql?
+ true
+ end
+
+ def compute_result
+ super.tap { |r| r.important_fields = group_fields }.grouped_by(all_group_fields(false), type, group_fields)
+ end
+
+ def sql_statement
+ super.tap do |sql|
+ define_group sql
+ sql.count unless sql.selects.include? 'count'
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/inherited_attribute.rb b/vendored-plugins/reporting_engine/lib/report/inherited_attribute.rb
new file mode 100644
index 0000000000..2e8bec000b
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/inherited_attribute.rb
@@ -0,0 +1,81 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'set'
+
+module Report::InheritedAttribute
+ include Report::QueryUtils
+
+ def inherited_attribute(*attributes)
+ options = attributes.extract_options!
+ list = options[:list]
+ merge = options.include?(:merge) ? options[:merge] : options[:list]
+ default = options[:default]
+ uniq = options[:uniq]
+ map = options[:map] || proc { |e| e }
+ default ||= [] if list
+ attributes.each do |name|
+ define_singleton_method(name) do |*values|
+ # FIXME: I'm ugly
+ return get_inherited_attribute(name, default, list, uniq) if values.empty?
+ if list
+ old = instance_variable_get("@#{name}") if merge
+ old ||= []
+ return set_inherited_attribute(name, values.map(&map) + old)
+ end
+ raise ArgumentError, "wrong number of arguments (#{values.size} for 1)" if values.size > 1
+ set_inherited_attribute name, map.call(values.first)
+ end
+ define_method(name) { |*values| self.class.send(name, *values) }
+ end
+ end
+
+ def define_singleton_method(name, &block)
+ singleton_class.send :attr_writer, name
+ singleton_class.class_eval { define_method(name, &block) }
+ define_method(name) { instance_variable_get("@#{name}") or singleton_class.send(name) }
+ end
+
+ def get_inherited_attribute(name, default = nil, list = false, uniq = false)
+ return get_inherited_attribute(name, default, list, false).uniq if list and uniq
+ result = instance_variable_get("@#{name}")
+ super_result = superclass.get_inherited_attribute(name, default, list) if inherit? name
+ if result.nil?
+ super_result || default
+ else
+ list && super_result ? result + super_result : result
+ end
+ end
+
+ def inherit?(name)
+ superclass.respond_to? :get_inherited_attribute and not not_inherited.include? name
+ end
+
+ def not_inherited
+ @not_inherited ||= Set.new
+ end
+
+ def dont_inherit(*attributes)
+ not_inherited.merge attributes
+ end
+
+ def set_inherited_attribute(name, value)
+ instance_variable_set "@#{name}", value
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/operator.rb b/vendored-plugins/reporting_engine/lib/report/operator.rb
new file mode 100644
index 0000000000..95d525cb05
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/operator.rb
@@ -0,0 +1,374 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Operator
+ include Report::QueryUtils
+ include Report::Validation
+ extend Forwardable
+
+ #############################################################################################
+ # Wrapped so we can place this at the top of the file.
+ def self.define_operators # :nodoc:
+ # Defaults
+ defaults do
+ def_delegators :'singleton_class', :forced?, :force!, :forced
+
+ def sql_operator
+ name
+ end
+
+ def where_clause
+ "%s %s '%s'"
+ end
+
+ def modify(query, field, *values)
+ query.where [where_clause, field, sql_operator, *values]
+ query
+ end
+
+ def label
+ @label ||= self.class.name
+ end
+ end
+
+ # Operators from Redmine
+ new '>t-', label: :label_less_than_ago do
+ include DateRange
+ def modify(query, field, value)
+ super query, field, -value.to_i, 0
+ end
+ end
+
+ new 'w', arity: 0, label: :label_this_week do
+ def modify(query, field, offset = nil)
+ offset ||= 0
+ first_day = begin
+ Integer I18n.t(:general_first_day_of_week)
+ rescue ArgumentError
+ 1 # assume mondays
+ end
+
+ from = Time.now.at_beginning_of_week + ((first_day % 7) - 1).days
+ from -= offset.days
+ '<>d'.to_operator.modify query, field, from, from + 7.days
+ end
+ end
+
+ new 't+', label: :label_in do
+ include DateRange
+ def modify(query, field, *values)
+ super query, field, values.first.to_i, values.first.to_i
+ end
+ end
+
+ new '<=', label: :label_less_or_equal
+
+ new '!', label: :label_not_equals do
+ def modify(query, field, *values)
+ where_clause = "(#{field} IS NULL"
+ where_clause += " OR #{field} NOT IN #{collection(*values)}" unless values.compact.empty?
+ where_clause += ')'
+ query.where where_clause
+ query
+ end
+ end
+
+ new 't-', label: :label_ago do
+ include DateRange
+ def modify(query, field, *values)
+ super query, field, -values.first.to_i, -values.first.to_i
+ end
+ end
+
+ new '!~', arity: 1, label: :label_not_contains do
+ def modify(query, field, *values)
+ value = values.first || ''
+ query.where "LOWER(#{field}) NOT LIKE '%#{quote_string(value.to_s.downcase)}%'"
+ query
+ end
+ end
+
+ new '=', label: :label_equals do
+ def modify(query, field, *values)
+ case
+ when values.size == 1 && values.first.nil?
+ query.where "#{field} IS NULL"
+ when values.compact.empty?
+ query.where '1=0'
+ else
+ query.where "#{field} IN #{collection(*values)}"
+ end
+ query
+ end
+ end
+
+ new '~', arity: 1, label: :label_contains do
+ def modify(query, field, *values)
+ value = values.first || ''
+ query.where "LOWER(#{field}) LIKE '%#{quote_string(value.to_s.downcase)}%'"
+ query
+ end
+ end
+
+ new '=', label: :label_greater_or_equal
+
+ new '!*', arity: 0, where_clause: '%s IS NULL', label: :label_none
+
+ new 't+', label: :label_in_more_than do
+ include DateRange
+ def modify(query, field, value)
+ super query, field, value.to_i, nil
+ end
+ end
+
+ new '*', arity: 0, where_clause: '%s IS NOT NULL', label: :label_all
+
+ # Our own operators
+ new '<', label: :label_less
+ new '>', label: :label_greater
+
+ new '=n', label: :label_equals do
+ def modify(query, field, value)
+ query.where "#{field} = #{clean_currency(value)}"
+ query
+ end
+ end
+
+ new '0', label: :label_none, where_clause: '%s = 0'
+ new 'y', label: :label_yes, arity: 0, where_clause: '%s IS NOT NULL'
+ new 'n', label: :label_no, arity: 0, where_clause: '%s IS NULL'
+
+ new 'd', label: :label_greater_or_equal, validate: :dates do
+ def modify(query, field, value)
+ return query if value.to_s.empty?
+ '>='.to_operator.modify query, field, quoted_date(value)
+ end
+ end
+
+ new '<>d', label: :label_between, validate: :dates do
+ def modify(query, field, from, to)
+ return query if from.to_s.empty? || to.to_s.empty?
+ query.where "#{field} BETWEEN '#{quoted_date from}' AND '#{quoted_date to}'"
+ query
+ end
+ end
+
+ new '=d', label: :label_date_on, validate: :dates do
+ def modify(query, field, value)
+ return query if value.to_s.empty?
+ '='.to_operator.modify query, field, quoted_date(value)
+ end
+ end
+
+ new '>=d', label: :label_days_ago, validate: :integers do
+ force! :integers
+
+ def modify(query, field, value)
+ now = Time.now
+ from = (now - value.to_i.days).beginning_of_day
+ '<>d'.to_operator.modify query, field, from, now
+ end
+ end
+
+ new '?=', label: :label_null_or_equal do
+ def modify(query, field, *values)
+ where_clause = "(#{field} IS NULL"
+ where_clause += " OR #{field} IN #{collection(*values)}" unless values.compact.empty?
+ where_clause += ')'
+ query.where where_clause
+ query
+ end
+ end
+
+ new '?!', label: :label_not_null_and_not_equal do
+ def modify(query, field, *values)
+ where_clause = "(#{field} IS NOT NULL"
+ where_clause += " AND #{field} NOT IN #{collection(*values)}" unless values.compact.empty?
+ where_clause += ')'
+ query.where where_clause
+ query
+ end
+ end
+ end
+ #############################################################################################
+
+ module CoreExt
+ ::String.send :include, self
+ ::Symbol.send :include, self
+ def to_operator
+ Report::Operator.find self
+ end
+ end
+
+ def self.force!(type)
+ @force = type
+ end
+
+ def self.forced?
+ !!@force
+ end
+
+ def self.forced
+ @force
+ end
+
+ def self.new(name, values = {}, &block)
+ all[name.to_s] ||= super
+ end
+
+ # TODO: this should be inheritable by subclasses
+ def self.all
+ @@all_operators ||= {}
+ end
+
+ def self.load
+ return if @done
+ @done = true
+ define_operators
+ end
+
+ def self.find(name)
+ all[name.to_s] or raise ArgumentError, "Operator #{name.inspect} not defined"
+ end
+
+ def self.exists?(name)
+ all.has_key?(name.to_s)
+ end
+
+ def self.defaults(&block)
+ class_eval &block
+ end
+
+ def self.default_operator
+ find '='
+ end
+
+ def self.integer_operators
+ ['<', '>', '<=', '>='].map(&:to_operator)
+ end
+
+ def self.null_operators
+ ['*', '!*'].map(&:to_operator)
+ end
+
+ def self.string_operators
+ ['!~', '~'].map(&:to_operator)
+ end
+
+ def self.time_operators
+ # ["t-", "t+", ">t-", "t+", "d', '>d', '=d'].map(&:to_operator)
+ end
+
+ def self.default_operators
+ ['=', '!'].map(&:to_operator)
+ end
+
+ attr_reader :name
+
+ def initialize(name, values = {}, &block)
+ @name = name.to_s
+ validation_methods = values.delete(:validate)
+ register_validations(validation_methods) unless validation_methods.nil?
+ values.each do |key, value|
+ singleton_class.class_eval { define_method(key) { value } }
+ end
+ singleton_class.class_eval(&block) if block
+ end
+
+ def to_operator
+ self
+ end
+
+ def to_s
+ name
+ end
+
+ def arity
+ @arity ||= begin
+ num = method(:modify).arity
+ # modify takes two more arguments before the values
+ num < 0 ? num + 2 : num - 2
+ end
+ end
+
+ def inspect
+ "#<#{self.class.name}:#{name.inspect}>"
+ end
+
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ## Creates an alias for a given operator.
+ def aka(alt_name, alt_label)
+ all = self.class.all
+ alt = alt_name.to_s
+ raise ArgumentError, "Can't alias operator with an existing one's name ( #{alt} )." if all.has_key?(alt)
+ op = all[name].clone
+ op.send(:rename_to, alt_name)
+ op.singleton_class.send(:define_method, 'label') { alt_label }
+ all[alt] = op
+ end
+
+ module DateRange
+ def modify(query, field, from, to)
+ query.where ["#{field} > '%s'", quoted_date((Date.yesterday + from).to_time.end_of_day)] if from
+ query.where ["#{field} <= '%s'", quoted_date((Date.today + to).to_time.end_of_day)] if to
+ query
+ end
+ end
+
+ private
+
+ def rename_to(new_name)
+ @name = new_name
+ end
+
+ # Done with class method definition, let's initialize the operators
+ load
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/query_utils.rb b/vendored-plugins/reporting_engine/lib/report/query_utils.rb
new file mode 100644
index 0000000000..aa1e61597c
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/query_utils.rb
@@ -0,0 +1,337 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Report::QueryUtils
+ Infinity = 1.0 / 0
+ include Engine
+
+ alias singleton_class metaclass unless respond_to? :singleton_class
+
+ delegate :quoted_false, :quoted_true, to: 'engine.reporting_connection'
+ attr_writer :engine
+
+ module PropagationHook
+ include Report::QueryUtils
+
+ def append_features(base)
+ ancestors[1..-1].reverse_each { |m| base.send(:include, m) }
+ base.extend PropagationHook
+ base.extend self
+ super
+ end
+
+ def propagate!(to = engine)
+ to.local_constants.each do |name|
+ const = to.const_get name
+ next unless Module === const
+ append_features const unless const <= self or not const < Report::QueryUtils
+ propagate! const
+ end
+ end
+ end
+
+ extend PropagationHook
+
+ ##
+ # Graceful string quoting.
+ #
+ # @param [Object] str String to quote
+ # @return [Object] Quoted version
+ def quote_string(str)
+ return str unless str.respond_to? :to_str
+ engine.reporting_connection.quote_string(str)
+ end
+
+ def current_language
+ ::I18n.locale
+ end
+
+ ##
+ # Creates a SQL fragment representing a collection/array.
+ #
+ # @see quote_string
+ # @param [#flatten] *values Ruby collection
+ # @return [String] SQL collection
+ def collection(*values)
+ return '' if values.empty?
+
+ v = if values.is_a?(Array)
+ values.flatten.each_with_object([]) do |str, l|
+ l << split_with_safe_return(str)
+ end
+ else
+ split_with_safe_return(str)
+ end
+
+ "(#{v.flatten.map { |x| "'#{quote_string(x)}'" }.join(', ')})"
+ end
+
+ def split_with_safe_return(str)
+ # From ruby doc:
+ # When the input str is empty an empty Array is returned as the string is
+ # considered to have no fields to split.
+ str.to_s.empty? ? '' : str.to_s.split(',')
+ end
+
+ ##
+ # Graceful, internationalized quoted string.
+ #
+ # @see quote_string
+ # @param [Object] str String to quote/translate
+ # @return [Object] Quoted, translated version
+ def quoted_label(ident)
+ "'#{quote_string ::I18n.t(ident)}'"
+ end
+
+ def quoted_date(date)
+ engine.reporting_connection.quoted_date date.to_dateish
+ end
+
+ ##
+ # SQL date quoting.
+ # @param [Date,Time] date Date to quote.
+ # @return [String] Quoted date.
+ def quote_date(date)
+ "'#{quoted_date date}'"
+ end
+
+ ##
+ # Generate a table name for any object.
+ #
+ # @example Table names
+ # table_name_for Issue # => 'issues'
+ # table_name_for :issue # => 'issues'
+ # table_name_for "issue" # => 'issues'
+ # table_name_for "issues" # => 'issues
+ #
+ # @param [#table_name, #to_s] object Object you need the table name for.
+ # @return [String] The table name.
+ def table_name_for(object)
+ return object.table_name if object.respond_to? :table_name
+ object.to_s.tableize
+ end
+
+ ##
+ # Generate a field name
+ #
+ # @example Field names
+ # field_name_for nil # => 'NULL'
+ # field_name_for 'foo' # => 'foo'
+ # field_name_for [Issue, 'project_id'] # => 'issues.project_id'
+ # field_name_for [:issue, 'project_id'], :entry # => 'issues.project_id'
+ # field_name_for 'project_id', :entry # => 'entries.project_id'
+ #
+ # @param [Array, Object] arg Object to generate field name for.
+ # @param [Object, optional] default_table Table name to use if no table name is given.
+ # @return [String] Field name.
+ def field_name_for(arg, default_table = nil)
+ return 'NULL' unless arg
+ return field_name_for(arg.keys.first, default_table) if arg.is_a? Hash
+ return arg if arg.is_a? String and arg =~ /\.| |\(.*\)/
+ return table_name_for(arg.first || default_table) + '.' << arg.last.to_s if arg.is_a? Array and arg.size == 2
+ return arg.to_s unless default_table
+ field_name_for [default_table, arg]
+ end
+
+ ##
+ # Sanitizes sql condition
+ #
+ # @see ActiveRecord::Base#sanitize_sql_for_conditions
+ # @param [Object] statement Not sanitized statement.
+ # @return [String] Sanitized statement.
+ def sanitize_sql_for_conditions(statement)
+ engine.send :sanitize_sql_for_conditions, statement
+ end
+
+ ##
+ # FIXME: This is redmine
+ # Generates string representation for a currency.
+ #
+ # @see CostRate.clean_currency
+ # @param [BigDecimal] value
+ # @return [String]
+ def clean_currency(value)
+ CostRate.clean_currency(value).to_f.to_s
+ end
+
+ ##
+ # Generates a SQL case statement.
+ #
+ # @example
+ # switch "#{table}.overridden_costs IS NULL" => [model, :costs], :else => [model, :overridden_costs]
+ #
+ # @param [Hash] options Condition => Result.
+ # @return [String] Case statement.
+ def switch(options)
+ desc = "#{__method__} #{options.inspect[1..-2]}".gsub(/(Cost|Time)Entry\([^\)]*\)/, '\1Entry')
+ options = options.with_indifferent_access
+ else_part = options.delete :else
+ "-- #{desc}\n\t" \
+ "CASE #{options.map { |k, v|
+ "\n\t\tWHEN #{field_name_for k}\n\t\t" \
+ "THEN #{field_name_for v}"
+ }.join(', ')}\n\t\tELSE #{field_name_for else_part}\n\tEND"
+ end
+
+ def iso_year_week(field, default_table = nil)
+ field = field_name_for(field, default_table)
+ "-- code specific for #{adapter_name}\n\t" << super(field)
+ end
+
+ ##
+ # Converts value with a given behavior, but treats nil differently.
+ # Params
+ # - value: the value to convert
+ # - weight_of_nil (optional): How a nil should be treated.
+ # :infinit - makes a nil weight really heavy, which will make it stay
+ # at the very end when sorting
+ # :negative_infinit - opposite of :infinit, let's the nil stay at the very beginning
+ # any other object - nil's will be replaced by thyt object
+ # - block (optional) - defines how to convert values which are not nil
+ # if no block is given, values stay untouched
+ def convert_unless_nil(value, weight_of_nil = :infinit)
+ if value.nil?
+ if weight_of_nil == :infinit
+ 1.0 / 0 # Infinity, which is greater than any string or number
+ elsif weight_of_nil == :negative_infinit
+ -1.0 / 0 # negative Infinity, which is smaller than any string or number
+ else
+ weight_of_nil
+ end
+ else
+ if block_given?
+ yield value
+ else
+ value
+ end
+ end
+ end
+
+ def map_field(key, value)
+ case key.to_s
+ when 'singleton_value', /_id$/ then convert_unless_nil(value) { |v| v.to_i }
+ else convert_unless_nil(value) { |v| v.to_s }
+ end
+ end
+
+ def adapter_name
+ engine.reporting_connection.adapter_name.downcase.to_sym
+ end
+
+ def cache
+ Report::QueryUtils.cache
+ end
+
+ def compare(first, second)
+ first = Array(first).flatten
+ second = Array(second).flatten
+ first.zip second do |a, b|
+ return (a <=> b) || (a == Infinity ? 1 : -1) if a != b
+ end
+ second.size > first.size ? -1 : 0
+ end
+
+ def mysql?
+ [:mysql, :mysql2].include? adapter_name.to_s.downcase.to_sym
+ end
+
+ def sqlite?
+ adapter_name == :sqlite
+ end
+
+ def postgresql?
+ adapter_name == :postgresql
+ end
+
+ module SQL
+ def typed(_type, value, escape = true)
+ escape ? "'#{quote_string value}'" : value
+ end
+ end
+
+ module MySql
+ include SQL
+ def iso_year_week(field)
+ "yearweek(#{field}, 1)"
+ end
+ end
+
+ module Sqlite
+ include SQL
+ def iso_year_week(field)
+ # enjoy
+ <<-EOS
+ case
+ when strftime('%W', strftime('%Y-01-04', #{field})) = '00' then
+ -- 01/01 is in week 1 of the current year => %W == week - 1
+ case
+ when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-04') = '00' then
+ -- we are at the end of the year, and it's the first week of the next year
+ (strftime('%Y', #{field}) + 1) || '01'
+ when strftime('%W', #{field}) < '08' then
+ -- we are in week 1 to 9
+ strftime('%Y0', #{field}) || (strftime('%W', #{field}) + 1)
+ else
+ -- we are in week 10 or later
+ strftime('%Y', #{field}) || (strftime('%W', #{field}) + 1)
+ end
+ else
+ -- 01/01 is in week 53 of the last year
+ case
+ when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-01') = '00' then
+ -- we are at the end of the year, and it's the first week of the next year
+ (strftime('%Y', #{field}) + 1) || '01'
+ when strftime('%W', #{field}) = '00' then
+ -- we are in the week belonging to last year
+ (strftime('%Y', #{field}) - 1) || '53'
+ else
+ -- everything is fine
+ strftime('%Y%W', #{field})
+ end
+ end
+ EOS
+ end
+ end
+
+ module Postres
+ include SQL
+ def typed(type, value, escape = true)
+ "#{super}::#{type}"
+ end
+
+ def iso_year_week(field)
+ "(EXTRACT(isoyear from #{field})*100 + \n\t\t" \
+ "EXTRACT(week from #{field} - \n\t\t" \
+ "(EXTRACT(dow FROM #{field})::int+6)%7))"
+ end
+ end
+
+ include MySql if mysql?
+ include Sqlite if sqlite?
+ include Postres if postgresql?
+
+ def self.cache
+ @cache ||= Hash.new { |h, k| h[k] = {} }
+ end
+
+ def self.included(klass)
+ super
+ klass.extend self
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/result.rb b/vendored-plugins/reporting_engine/lib/report/result.rb
new file mode 100644
index 0000000000..c8a0bfde69
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/result.rb
@@ -0,0 +1,299 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Result
+ include Report::QueryUtils
+
+ class Base
+ attr_accessor :parent, :type, :important_fields
+ attr_accessor :key
+ attr_reader :value
+ alias values value
+ include Enumerable
+ include Report::QueryUtils
+
+ def initialize(value)
+ @important_fields ||= []
+ @type = :direct
+ @value = value
+ end
+
+ def recursive_each_with_level(level = 0, _depth_first = true, &block)
+ block.call(level, self)
+ end
+
+ def recursive_each
+ recursive_each_with_level { |_level, result| yield result }
+ end
+
+ def to_hash
+ fields.dup
+ end
+
+ def [](key)
+ fields[key]
+ end
+
+ ##
+ # Override if you want to influence the result grouping.
+ #
+ # @return A value for grouping or nil if the given field should
+ # not be considered for grouping.
+ def map_group_by_value(_key, value)
+ value
+ end
+
+ ##
+ # This method is called when this result is requested as #grouped_by something
+ # just before the result is returned.
+ #
+ # @param data This result's grouped data.
+ def group_by_data_ready(_data)
+ # good to know!
+ end
+
+ def grouped_by(fields, type, important_fields = [])
+ @grouped_by ||= {}
+ list = begin
+ @grouped_by[fields] ||= begin
+ # sub results, have fields
+ # i.e. grouping by foo, bar
+ data = group_by do |entry|
+ # index for group is a hash
+ # i.e. { :foo => 10, :bar => 20 } <= this is just the KEY!!!!
+ fields.inject({}) do |hash, key|
+ val = map_group_by_value(key, entry.fields[key])
+ hash.merge key => val
+ end
+ end
+ group_by_data_ready(data)
+ # map group back to array, all fields with same key get grouped into one list
+ data.keys.map { |f| engine::Result.new data[f], f, type, important_fields }
+ end
+ end
+ # create a single result from that list
+ engine::Result.new list, {}, type, important_fields
+ end
+
+ def inspect
+ "<##{self.class}: @fields=#{fields.inspect} @type=#{type.inspect} " \
+ "@size=#{size} @count=#{count} @units=#{units}>"
+ end
+
+ def row?
+ type == :row
+ end
+
+ def column?
+ type == :column
+ end
+
+ def direct?
+ type == :direct
+ end
+
+ def each_row
+ end
+
+ def final?(type)
+ type? type and (direct? or size == 0 or first.type != type)
+ end
+
+ def type?(type)
+ self.type == type
+ end
+
+ def depth_of(type)
+ if type? type or (type == :column and direct?) then 1
+ else 0
+ end
+ end
+
+ def final_number(type)
+ return 1 if final? type
+ return 0 if direct?
+ @final_number ||= {}
+ @final_number[type] ||= sum { |v| v.final_number type }
+ end
+
+ def final_row?
+ final? :row
+ end
+
+ def final_column?
+ final? :column
+ end
+
+ def render(keys = important_fields)
+ fields.map { |k, v| yield(k, v) if keys.include? k }.join
+ end
+
+ def set_key(index = [])
+ self.key = index.map { |k| map_field(k, fields[k]) }
+ end
+ end
+
+ class DirectResult < Base
+ alias fields values
+
+ def has_children?
+ false
+ end
+
+ def count
+ self['count'].to_i
+ end
+
+ def units
+ self['units'].to_d
+ end
+
+ ##
+ # @return [Integer] Number of child results
+ def size
+ 0
+ end
+
+ def each
+ return enum_for(__method__) unless block_given?
+ yield self
+ end
+
+ def each_direct_result(_cached = false)
+ return enum_for(__method__) unless block_given?
+ yield self
+ end
+
+ def sort!(force = false)
+ force
+ end
+ end
+
+ class WrappedResult < Base
+ include Enumerable
+
+ def set_key(index = [])
+ values.each { |v| v.set_key index }
+ super
+ end
+
+ def sort!(force = false)
+ return false if @sorted and not force
+ values.sort! { |a, b| compare a.key, b.key }
+ values.each { |e| e.sort! force }
+ @sorted = true
+ end
+
+ def depth_of(type)
+ super + first.depth_of(type)
+ end
+
+ def has_children?
+ true
+ end
+
+ def count
+ sum_for :count
+ end
+
+ def units
+ sum_for :units
+ end
+
+ def sum_for(field)
+ @sum_for ||= {}
+ @sum_for[field] ||= sum { |v| v.send(field) || 0 }
+ end
+
+ def recursive_each_with_level(level = 0, depth_first = true, &block)
+ if depth_first
+ super
+ each { |c| c.recursive_each_with_level(level + 1, depth_first, &block) }
+ else # width-first
+ to_evaluate = [self]
+ lvl = level
+ while !to_evaluate.empty?
+ # evaluate all stored results and find the results we need to evaluate soon
+ to_evaluate_soon = []
+ to_evaluate.each do |r|
+ block.call(lvl, r)
+ to_evaluate_soon.concat r.values if r.size > 0
+ end
+ # take new results to evaluate
+ lvl = lvl + 1
+ to_evaluate = to_evaluate_soon
+ end
+ end
+
+ def each_row
+ return enum_for(:each_row) unless block_given?
+ if final_row? then yield self
+ else each { |c| c.each_row(&Proc.new) }
+ end
+ end
+ end
+
+ def to_a
+ values
+ end
+
+ def each(&block)
+ values.each(&block)
+ end
+
+ def each_direct_result(cached = true)
+ return enum_for(__method__) unless block_given?
+ if @direct_results
+ @direct_results.each { |r| yield(r) }
+ else
+ values.each do |value|
+ value.each_direct_result(false) do |result|
+ (@direct_results ||= []) << result if cached
+ yield result
+ end
+ end
+ end
+ end
+
+ def fields
+ @fields ||= {}.with_indifferent_access
+ end
+
+ ##
+ # @return [Integer] Number of child results
+ def size
+ values.size
+ end
+ end
+
+ def self.new(value, fields = {}, type = nil, important_fields = [])
+ result = begin
+ case value
+ when ActiveRecord::Result, Array then engine::Result::WrappedResult.new value.map { |e| new e, {}, nil, important_fields }
+ when Hash then engine::Result::DirectResult.new value.with_indifferent_access
+ when Base then value
+ else raise ArgumentError, "Cannot create Result from #{value.inspect}"
+ end
+ end
+ result.fields.merge! fields
+ result.type = type if type
+ result.important_fields = important_fields unless result == value
+ result
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/sql_statement.rb b/vendored-plugins/reporting_engine/lib/report/sql_statement.rb
new file mode 100644
index 0000000000..f85d6aef2e
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/sql_statement.rb
@@ -0,0 +1,288 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::SqlStatement
+ class Union
+ attr_accessor :first, :second, :as
+ def initialize(first, second, as = nil)
+ @first, @second, @as = first, second, as
+ end
+
+ def to_s
+ "((\n#{first.gsub("\n", "\n\t")}\n) UNION (\n" \
+ "#{second.gsub("\n", "\n\t")}\n))#{" AS #{as}" if as}\n"
+ end
+
+ def each_subselect
+ yield first
+ yield second
+ end
+
+ def gsub(*args, &block)
+ to_s.gsub(*args, &block)
+ end
+ end
+
+ include Report::QueryUtils
+
+ ##
+ # Describes the query. This may be used in a sql-comment later.
+ attr_accessor :desc
+
+ ##
+ # Generates new SqlStatement.
+ #
+ # @param [String, #to_s] table Table name (or subselect) for from part.
+ def initialize(table, desc = '')
+ self.desc = desc
+ from table
+ end
+
+ ##
+ # Creates a uninon of the caller and the callee.
+ #
+ # @param [Report::SqlStatement] other Second part of the union
+ # @return [String] The sql query.
+ def union(other, as = nil)
+ Union.new(self, other, as)
+ end
+
+ ##
+ # Adds sum(..) part to select.
+ #
+ # @param [#to_s] field Name of the field to aggregate on
+ # @param [#to_s] name Name of the result (defaults to sum)
+ def sum(field, name = :sum, type = :sum)
+ @sql = nil
+ return sum({ name => field }, nil, type) unless field.respond_to? :to_hash
+ field.each { |k, v| field[k] = "#{type}(#{v})" }
+ select field
+ end
+
+ ##
+ # Adds count(..) part to select.
+ #
+ # @param [#to_s] field Name of the field to aggregate on (defaults to *)
+ # @param [#to_s] name Name of the result (defaults to sum)
+ def count(field = '*', name = :count)
+ sum field, name, :count
+ end
+
+ ##
+ # Generates the SQL query.
+ # Code looks ugly in exchange for pretty output (so one does unterstand those).
+ #
+ # @return [String] The query
+ def to_s
+ # FIXME I'm ugly
+ @sql ||= begin
+ sql = "\n-- BEGIN #{desc}\n" \
+ "-- DB: #{ActiveRecord::Base.connection.current_database}\n" \
+ "SELECT\n#{select.map { |e| "\t#{e}" }.join ",\n"}" \
+ "\nFROM\n\t#{from.gsub("\n", "\n\t")}" \
+ "\n\t#{joins.map { |e| e.gsub("\n", "\n\t") }.join "\n\t"}" \
+ "\nWHERE #{where.join ' AND '}\n"
+ sql << "GROUP BY #{group_by.join ', '}\nORDER BY #{group_by.join ', '}\n" if group_by?
+ sql << "-- END #{desc}\n"
+ sql.gsub!('--', '#') if mysql?
+ sql # << " LIMIT 100"
+ end
+ end
+
+ ##
+ # @overload from
+ # Reads the from part.
+ # @return [#to_s] From part
+ # @overload from(table)
+ # Sets the from part.
+ # @param [#to_s] table
+ # @param [#to_s] From part
+ def from(table = nil)
+ return @from unless table
+ @sql = nil
+ @from = table
+ end
+
+ ##
+ # Where conditions. Will be joined together by AND.
+ #
+ # @overload where
+ # Reads the where part
+ # @return [Array<#to_s>] Where clauses
+ # @overload where(fields)
+ # Adds condition to where clause
+ # @param [Array, Hash, String] fields Parameters passed to sanitize_sql_for_conditions.
+ # @see Report::QueryUtils#sanitize_sql_for_conditions
+ def where(fields = nil)
+ @where ||= ['1=1']
+ unless fields.nil?
+ @where << sanitize_sql_for_conditions(fields)
+ @sql = nil
+ end
+ @where
+ end
+
+ ##
+ # @return [Array] List of table joins
+ def joins
+ (@joins ||= []).tap(&:uniq!)
+ end
+
+ ##
+ # Adds an "left outer join" (guessing field names) to #joins.
+ #
+ # @overload join(name)
+ # @param [Symbol, String] name Singular table name to join with, will join plural from on table.id = table_id
+ # @overload join(model)
+ # @param [#table_name, #model_name] model ActiveRecord model to join with
+ # @overload join(hash)
+ # @param [Hash<#to_s => #to_s>] hash Key is singular table name to join with, value is field to join on
+ # @overload join(*list)
+ # @param [Array] list Will generate join entries (according to guessings described above)
+ # @see #joins
+ def join(*list)
+ @sql = nil
+ join_syntax = 'LEFT OUTER JOIN %1$s ON %1$s.id = %2$s_id'
+ list.each do |e|
+ case e
+ when Class then joins << (join_syntax % [table_name_for(e), e.lookup_ancestors.last.model_name.to_s.underscore])
+ when / / then joins << e
+ when Symbol, String then joins << (join_syntax % [table_name_for(e), e])
+ when Hash then e.each { |k, v| joins << (join_syntax % [table_name_for(k), field_name_for(v)]) }
+ when Array then join(*e)
+ else raise ArgumentError, "cannot join #{e.inspect}"
+ end
+ end
+ end
+
+ def default_select(value = nil)
+ @default_select = value if value
+ @default_select ||= ['*']
+ end
+
+ ##
+ # @overload select
+ # @return [Array] All fields/statements for select part
+ #
+ # @overload select(*fields)
+ # Adds fields to select query.
+ # @example
+ # SqlStatement.new.select(some_sql_statement) # [some_sql_statement.to_s]
+ # SqlStatement.new.select("sum(foo)") # ["sum(foo)"]
+ # SqlStatement.new.select(:a).select(:b) # ["a", "b"]
+ # SqlStatement.new.select(:bar => :foo) # ["foo as bar"]
+ # SqlStatement.new.select(:bar => nil) # ["NULL as bar"]
+ # @param [Array, Hash, String, Symbol, SqlStatement] fields Fields to add to select part
+ # @return [Array] All fields/statements for select part
+ def select(*fields)
+ return(@select || default_select) if fields.empty?
+ (@select ||= []).tap do
+ @sql = nil
+ fields.reject { |f| never_select.include? f }.each do |f|
+ case f
+ when Array
+ if f.size == 2 and f.first.respond_to? :table_name then select field_name_for(f)
+ else select(*f)
+ end
+ when Hash then select f.map { |k, v| "#{field_name_for v} as #{field_name_for k}" }
+ when String, Symbol then @select << field_name_for(f)
+ when engine::SqlStatement then @select << f.to_s
+ else raise ArgumentError, "cannot handle #{f.inspect}"
+ end
+ end
+ # when doing a union in sql, both subselects must have the same order.
+ # by sorting here we never ever have to worry about this again, sucker!
+ @select = @select.uniq.sort_by { |x| x.split(' as ').last }
+ end
+ end
+
+ def unselect(*fields)
+ @sql = nil
+ @select = @select.reject do |field|
+ fields.find { |f| f == field }
+ end
+ end
+
+ def never_select(*fields)
+ (@never_select ||= []).tap do
+ unless fields.empty?
+ @never_select += fields
+ unselect *fields
+ end
+ end
+ end
+
+ ##
+ # Return the names which have been bound through select statements
+ # @return [Array] All fields for select part
+ def selects
+ @select.map { |s| s.split(' as ').last }
+ end
+
+ ##
+ # @overload group_by
+ # @return [Array] All fields/statements for group by part
+ #
+ # @overload group(*fields)
+ # Adds fields to group by query
+ # @param [Array, String, Symbol] fields Fields to add
+ def group_by(*fields)
+ @sql = nil unless fields.empty?
+ (@group_by ||= []).tap do
+ fields.reject { |f| never_group_by.include? f }.each do |e|
+ if e.is_a? Array and (e.size != 2 or !e.first.respond_to? :table_name)
+ group_by(*e)
+ else
+ @group_by << field_name_for(e)
+ end
+ end
+ @group_by.uniq!
+ end
+ end
+
+ def group_not_by(*fields)
+ @sql = nil
+ @group_by = @group_by.reject do |field|
+ fields.find { |f| f == field }
+ end
+ end
+
+ def never_group_by(*fields)
+ (@never_group_by ||= []).tap do
+ unless fields.empty?
+ @never_group_by += fields
+ group_not_by *fields
+ end
+ end
+ end
+
+ ##
+ # @return [TrueClass, FalseClass] Whether or not to add a group by part.
+ def group_by?
+ !group_by.empty?
+ end
+
+ def inspect
+ "#"
+ end
+
+ def gsub(*args, &block)
+ to_s.gsub(*args, &block)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/table.rb b/vendored-plugins/reporting_engine/lib/report/table.rb
new file mode 100644
index 0000000000..bd85aa398e
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/table.rb
@@ -0,0 +1,109 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# encoding: UTF-8
+require 'enumerator'
+
+class Report::Table
+ attr_accessor :query
+ include Report::QueryUtils
+
+ def initialize(query)
+ @query = query
+ end
+
+ def row_index
+ get_index :row
+ end
+
+ def column_index
+ get_index :column
+ end
+
+ def row_fields
+ fields_for :row
+ end
+
+ def column_fields
+ fields_for :column
+ end
+
+ def rows_for(result)
+ fields_for result, :row
+ end
+
+ def columns_for(result)
+ fields_for result, :column
+ end
+
+ def fields_from(result, type)
+ fields_for(type).map { |k| map_field k, result.fields[k] }
+ end
+
+ ##
+ # @param [Array] expected Fields expected
+ # @param [Array,Hash,Result] given Fields/result to be tested
+ # @return [TrueClass,FalseClass]
+ def satisfies?(type, expected, given)
+ given = fields_from(given, type) if given.respond_to? :to_hash
+ zipped = expected.zip given
+ zipped.all? { |a, b| a == b or b.nil? }
+ end
+
+ def fields_for(type)
+ @fields_for ||= begin
+ child, fields = query.chain, Hash.new { |h, k| h[k] = [] }
+ until child.filter?
+ fields[child.type].push(*child.group_fields)
+ child = child.child
+ end
+ fields
+ end
+ @fields_for[type]
+ end
+
+ def get_row(*args)
+ @query.each_row { |result| return with_gaps_for(type, result) if satisfies? :row, args, result }
+ []
+ end
+
+ def with_gaps_for(type, result)
+ return enum_for(:with_gaps_for, type, result) unless block_given?
+ stack = get_index(type).dup
+ result.each_direct_result do |subresult|
+ yield nil until stack.empty? or satisfies? type, stack.shift, subresult
+ yield subresult
+ end
+ stack.size.times { yield nil }
+ end
+
+ def [](x, y)
+ get_row(row_index[y]).first(x).last
+ end
+
+ def get_index(type)
+ @indexes ||= begin
+ indexes = Hash.new { |h, k| h[k] = Set.new }
+ query.each_direct_result { |result| [:row, :column].each { |t| indexes[t] << fields_from(result, t) } }
+ indexes.keys.each { |k| indexes[k] = indexes[k].sort { |x, y| compare x, y } }
+ indexes
+ end
+ @indexes[type]
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/transformer.rb b/vendored-plugins/reporting_engine/lib/report/transformer.rb
new file mode 100644
index 0000000000..c53edd7f68
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/transformer.rb
@@ -0,0 +1,65 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# encoding: UTF-8
+class Report::Transformer
+ attr_reader :query
+
+ def initialize(query)
+ @query = query
+ end
+
+ ##
+ # @return [Report::Result::Base] Result tree with row group bys at the top
+ # @see Report::Chainable#result
+ def row_first
+ @row_first ||= query.result
+ end
+
+ ##
+ # @return [Report::Result::Base] Result tree with column group bys at the top
+ # @see Report::Walker#row_first
+ def column_first
+ @column_first ||= begin
+ # reverse since we fake recursion ↓↓↓
+ list, all_fields = restructured.reverse, @all_fields.dup
+ result = list.inject(@ungrouped) do |aggregate, (current_fields, type)|
+ fields, all_fields = all_fields, all_fields - current_fields
+ aggregate.grouped_by fields, type, current_fields
+ end
+ result or query.result
+ end
+ end
+
+ ##
+ # Important side effect: it sets @ungrouped, @all_fields.
+ # @return [Array, Symbol>>] Group by fields + types (:row or :column)
+ def restructured
+ rows, columns, current = [], [], query.chain
+ @all_fields = []
+ until current.filter?
+ @ungrouped = current.result if current.responsible_for_sql?
+ list = current.row? ? rows : columns
+ list << [current.group_fields, current.type]
+ @all_fields.push(*current.group_fields)
+ current = current.child
+ end
+ columns + rows
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/validation.rb b/vendored-plugins/reporting_engine/lib/report/validation.rb
new file mode 100644
index 0000000000..958d02b315
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/validation.rb
@@ -0,0 +1,61 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Report::Validation
+ include Report::QueryUtils
+
+ def register_validations(*validation_methods)
+ validation_methods.flatten.each do |val_method|
+ register_validation(val_method)
+ end
+ end
+
+ def register_validation(val_method)
+ const_name = val_method.to_s.camelize
+ begin
+ val_module = engine::Validation.const_get const_name
+ singleton_class.send(:include, val_module)
+ val_method = 'validate_' + val_method.to_s.pluralize
+ if method(val_method)
+ validations << val_method
+ else
+ warn "#{val_module.name} does not define #{val_method}"
+ end
+ rescue NameError
+ warn "No Module #{engine}::Validation::#{const_name} found to validate #{val_method}"
+ end
+ self
+ end
+
+ def errors
+ @errors ||= Hash.new { |h, k| h[k] = [] }
+ end
+
+ def validations
+ @validations ||= []
+ end
+
+ def validate(*values)
+ errors.clear
+ return true if validations.empty?
+ validations.all? do |validation|
+ values.empty? ? true : send(validation, *values)
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/validation/dates.rb b/vendored-plugins/reporting_engine/lib/report/validation/dates.rb
new file mode 100644
index 0000000000..23ffd058b2
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/validation/dates.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Report::Validation
+ module Dates
+ def validate_dates(*values)
+ values = values.flatten
+ return true if values.empty?
+ values.flatten.all? do |val|
+ begin
+ !!val.to_dateish
+ rescue ArgumentError
+ errors[:date] << val
+ validate_dates(values - [val])
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/validation/integers.rb b/vendored-plugins/reporting_engine/lib/report/validation/integers.rb
new file mode 100644
index 0000000000..18f6af179f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/validation/integers.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Report::Validation
+ module Integers
+ def validate_integers(*values)
+ values = values.flatten
+ return true if values.empty?
+ values.flatten.all? do |val|
+ if val.to_i.to_s != val.to_s
+ errors[:int] << val
+ validate_integers(values - [val])
+ false
+ else
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/validation/sql.rb b/vendored-plugins/reporting_engine/lib/report/validation/sql.rb
new file mode 100644
index 0000000000..0944d79516
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/validation/sql.rb
@@ -0,0 +1,26 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module Report::Validation
+ module Sql
+ def validate_sql(_values = [])
+ raise NotImplementedError, "Haven't done SQL validation just yet!"
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/report/walker.rb b/vendored-plugins/reporting_engine/lib/report/walker.rb
new file mode 100644
index 0000000000..f9c3b6fb0f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/report/walker.rb
@@ -0,0 +1,119 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Report::Walker
+ attr_accessor :query, :header_stack
+ def initialize(query)
+ @query = query
+ end
+
+ def for_row(&block)
+ access_block(:row, &block)
+ end
+
+ def for_final_row(&block)
+ access_block(:final_row, &block) || access_block(:row)
+ end
+
+ def for_cell(&block)
+ access_block(:cell, &block)
+ end
+
+ def for_empty_cell(&block)
+ access_block(:empty_cell, &block) || access_block(:cell)
+ end
+
+ def access_block(name, &block)
+ @blocks ||= {}
+ @blocks[name] = block if block
+ @blocks[name]
+ end
+
+ def walk_cell(cell)
+ cell ? for_cell[cell] : for_empty_cell[nil]
+ end
+
+ def headers(result = nil, &_block)
+ @header_stack = []
+ result ||= query.column_first
+ sort result
+ last_level = -1
+ num_in_col = 0
+ level_size = 1
+ sublevel = 0
+ result.recursive_each_with_level(0, false) do |level, result|
+ break if result.final_column?
+ if first_in_col = (last_level < level)
+ list = []
+ last_level = level
+ num_in_col = 0
+ level_size = sublevel
+ sublevel = 0
+ @header_stack << list
+ end
+ num_in_col += 1
+ sublevel += result.size
+ last_in_col = (num_in_col >= level_size)
+ @header_stack.last << [result, first_in_col, last_in_col]
+ yield(result, level == 0, first_in_col, last_in_col) if block_given?
+ end
+ end
+
+ def reverse_headers
+ fail 'call header first' unless @header_stack
+ first = true
+ @header_stack.reverse_each do |list|
+ list.each do |result, first_in_col, last_in_col|
+ yield(result, first, first_in_col, last_in_col)
+ end
+ first = false
+ end
+ end
+
+ def headers_empty?
+ fail 'call header first' unless @header_stack
+ @header_stack.empty?
+ end
+
+ def sort_keys
+ @sort_keys ||= query.chain.map { |c| c.group_fields.map(&:to_s) if c.group_by? }.compact.flatten
+ end
+
+ def sort(result)
+ result.set_key sort_keys
+ result.sort!
+ end
+
+ def body(result = nil)
+ return [*body(result)].each { |a| yield a } if block_given?
+ result ||= query.result.tap { |r| sort(r) }
+ if result.row?
+ if result.final_row?
+ subresults = query.table.with_gaps_for(:column, result).map(&method(:walk_cell))
+ for_final_row.call result, subresults
+ else
+ subresults = result.map { |r| body(r) }
+ for_row.call result, subresults
+ end
+ else
+ # you only get here if no rows are defined
+ result.each_direct_result.map(&method(:walk_cell))
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine.rb b/vendored-plugins/reporting_engine/lib/reporting_engine.rb
new file mode 100644
index 0000000000..b9e885fb65
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module ReportingEngine
+ require 'reporting_engine/engine'
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/engine.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/engine.rb
new file mode 100644
index 0000000000..00482e0e7d
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/engine.rb
@@ -0,0 +1,63 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'rails/engine'
+
+module ReportingEngine
+ class Engine < ::Rails::Engine
+ engine_name :reportingengine
+
+ config.autoload_paths += Dir["#{config.root}/lib/"]
+
+ initializer 'reportingengine.precompile_assets' do
+ Rails.application.config.assets.precompile += %w(reporting_engine.css reporting_engine.js)
+ end
+
+ initializer 'check mysql version' do
+ connection = ActiveRecord::Base.connection
+ adapter_name = connection.adapter_name.to_s.downcase.to_sym
+ if [:mysql, :mysql2].include?(adapter_name)
+ # The reporting engine is incompatible with the
+ # following mysql versions due to a bug in MySQL itself:
+ # 5.6.0 - 5.6.12
+ # 5.7.0 - 5.7.1
+ # see https://www.openproject.org/issues/967 for details.
+ required_patch_levels = { '5.6' => 13, '5.7' => 2 }
+
+ mysql_version = connection.show_variable('VERSION')
+ release_version, patch_level = mysql_version.match(/(\d*\.\d*)\.(\d*)/).captures
+ required_patch_level = required_patch_levels[release_version]
+
+ if required_patch_level && (patch_level.to_i < required_patch_level)
+ raise "MySQL #{mysql_version} is not supported. Version #{release_version} \
+ requires patch level >= #{required_patch_level}."
+ end
+ end
+ end
+
+ config.to_prepare do
+ require 'reporting_engine/patches'
+ require 'reporting_engine/patches/big_decimal_patch'
+ require 'reporting_engine/patches/to_date_patch'
+ # We have to require this here because Ruby will otherwise find Date
+ # as Object::Date and Rails wont autoload Widget::Filters::Date
+ require_dependency 'widget/filters/date'
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/helpers/reporting_helper.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/helpers/reporting_helper.rb
new file mode 100644
index 0000000000..9d591a8dae
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/helpers/reporting_helper.rb
@@ -0,0 +1,30 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+##
+# A minimal ReportingHelper module. This is included in Widget and
+# Controller and can be used to extend the specific widgets and
+# controller functionality.
+#
+# It is the default hook for translations, and calls to l() in Widgets
+# or Controllers will go to this module, first. The default behavior
+# is to pass translation work on to I18n.t() or I18n.l(), depending on
+# the type of arguments.
+module ReportingHelper
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/patches.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/patches.rb
new file mode 100644
index 0000000000..08489f631b
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/patches.rb
@@ -0,0 +1,21 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module ReportingEngine::Patches
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/patches/big_decimal_patch.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/patches/big_decimal_patch.rb
new file mode 100644
index 0000000000..53df32ce07
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/patches/big_decimal_patch.rb
@@ -0,0 +1,40 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module ReportingEngine::Patches::BigDecimalPatch
+ module BigDecimal
+ ::BigDecimal.send :include, self
+ def to_d; self end
+ end
+
+ module Integer
+ ::Integer.send :include, self
+ def to_d; to_f.to_d end
+ end
+
+ module String
+ ::String.send :include, self
+ def to_d; ::BigDecimal.new(self) end
+ end
+
+ module NilClass
+ ::NilClass.send :include, self
+ def to_d; 0 end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/patches/to_date_patch.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/patches/to_date_patch.rb
new file mode 100644
index 0000000000..b9d5487606
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/patches/to_date_patch.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'date'
+
+module ReportingEngine::Patches::ToDatePatch
+ module StringAndNil
+ ::String.send(:include, self)
+ ::NilClass.send(:include, self)
+
+ def to_dateish
+ return Date.today if blank?
+ Date.parse self
+ end
+ end
+
+ module DateAndTime
+ ::Date.send(:include, self)
+ ::Time.send(:include, self)
+
+ def to_dateish
+ self
+ end
+
+ def force_utc
+ return to_time.force_utc unless respond_to? :utc_offset
+ return self if utc?
+ utc - utc_offset
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/tasks/spec.rake b/vendored-plugins/reporting_engine/lib/reporting_engine/tasks/spec.rake
new file mode 100644
index 0000000000..7514c6aebd
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/tasks/spec.rake
@@ -0,0 +1,45 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+begin
+ require 'spec/rake/spectask'
+ namespace :spec do
+ namespace :plugins do
+ desc 'Runs the examples for reporting_engine'
+ Spec::Rake::SpecTask.new(:reporting_engine) do |t|
+ t.spec_opts = ['--options', "\"#{Rails.root}/spec/spec.opts\""]
+ t.spec_files = FileList['vendor/plugins/reporting_engine/spec/**/*_spec.rb']
+ end
+
+ desc 'Runs the examples for reporting_engine'
+ Spec::Rake::SpecTask.new(:"reporting_engine:rcov") do |t|
+ t.spec_opts = ['--options', "\"#{Rails.root}/spec/spec.opts\""]
+ t.spec_files = FileList['vendor/plugins/reporting_engine/spec/**/*_spec.rb']
+ t.rcov = true
+ t.rcov_opts = ['-x', "\.rb,spec", '-i', 'reporting_engine/app/,redmine_reporting/lib/']
+ end
+ end
+ end
+ task spec: 'spec:plugins:reporting_engine'
+
+ require 'ci/reporter/rake/rspec' # use this if you're using RSpec
+ require 'ci/reporter/rake/test_unit' # use this if you're using Test::Unit
+ task :"spec:plugins:reporting_engine:ci" => ['ci:setup:rspec', 'spec:plugins:redmine_reporting']
+rescue LoadError
+end
diff --git a/vendored-plugins/reporting_engine/lib/reporting_engine/version.rb b/vendored-plugins/reporting_engine/lib/reporting_engine/version.rb
new file mode 100644
index 0000000000..153eb7f07d
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/reporting_engine/version.rb
@@ -0,0 +1,22 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+module ReportingEngine
+ VERSION = '5.0.10'
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget.rb b/vendored-plugins/reporting_engine/lib/widget.rb
new file mode 100644
index 0000000000..4cfdef9d2f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget.rb
@@ -0,0 +1,65 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require_dependency 'reporting_engine/helpers/reporting_helper'
+
+class Widget < ActionView::Base
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::FormTagHelper
+ include ActionView::Helpers::JavaScriptHelper
+ include Rails.application.routes.url_helpers
+ include ReportingHelper
+
+ attr_accessor :output_buffer, :controller, :config, :_content_for, :_routes, :subject
+
+ def self.new(subject)
+ super(subject).tap do |o|
+ o.subject = subject
+ end
+ end
+
+ def current_language
+ ::I18n.locale
+ end
+
+ def protect_against_forgery?
+ false
+ end
+
+ def method_missing(name, *args, &block)
+ controller.send(name, *args, &block)
+ rescue NoMethodError
+ raise NoMethodError, "undefined method `#{name}' for #<#{self.class}:0x#{object_id}>"
+ end
+
+ module RenderWidgetInstanceMethods
+ def render_widget(widget, subject, options = {}, &block)
+ i = widget.new(subject)
+ i.config = config
+ i._routes = _routes
+ i._content_for = @_content_for
+ i.controller = respond_to?(:controller) ? controller : self
+ i.render_with_options(options, &block)
+ end
+ end
+end
+
+ActionView::Base.send(:include, Widget::RenderWidgetInstanceMethods)
+ActionController::Base.send(:include, Widget::RenderWidgetInstanceMethods)
diff --git a/vendored-plugins/reporting_engine/lib/widget/base.rb b/vendored-plugins/reporting_engine/lib/widget/base.rb
new file mode 100644
index 0000000000..2254832e4c
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/base.rb
@@ -0,0 +1,151 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+require 'digest/sha1'
+
+class Widget::Base < Widget
+ attr_reader :engine, :output
+
+ ##
+ # Deactivate caching for certain widgets. If called on Widget::Base,
+ # caching is deactivated globally
+ def self.dont_cache!
+ @dont_cache = true
+ end
+
+ ##
+ # Query whether this widget class should be cached.
+ def self.dont_cache?
+ @dont_cache or self != Widget::Base && Widget::Base.dont_cache?
+ end
+
+ def initialize(query)
+ @subject = query
+ @engine = query.class
+ @options = {}
+ end
+
+ ##
+ # Write a string to the canvas. The string is marked as html_safe.
+ # This will write twice, if @cache_output is set.
+ def write(str)
+ str ||= ''
+ @output ||= ''.html_safe
+ @output = @output + '' if @output.frozen? # Rails 2 freezes tag strings
+ @output.concat str.html_safe
+ @cache_output.concat(str.html_safe) if @cache_output
+ str.html_safe
+ end
+
+ ##
+ # Render this widget. Abstract method. Needs to call #write at least once
+ def render
+ raise NotImplementedError, "#render is missing in my subclass #{self.class}"
+ end
+
+ ##
+ # Render this widget, passing options.
+ # Available options:
+ # :to => canvas - The canvas (streaming or otherwise) to render to. Has to respond to #write
+ def render_with_options(options = {}, &block)
+ @help_text = options[:help_text]
+ set_canvas(options.delete(:to)) if options.has_key? :to
+ @options = options
+ render_with_cache(options, &block)
+ @output
+ end
+
+ ##
+ # An optional help text. If defined the Help Widget
+ # displaying the given text is going to be placed
+ # next to this Widget, if it supports that.
+ def help_text
+ @help_text
+ end
+
+ def help_text=(text)
+ @help_text = text
+ end
+
+ def cache_key
+ @cache_key ||= Digest::SHA1::hexdigest begin
+ if subject.respond_to? :cache_key
+ "#{I18n.locale}/#{self.class.name.demodulize}/#{subject.cache_key}/#{@options.sort_by(&:to_s)}"
+ else
+ subject.inspect
+ end
+ end
+ end
+
+ def cached?
+ cache? && Rails.cache.exist?(cache_key)
+ end
+
+ private
+
+ def cache?
+ !self.class.dont_cache?
+ end
+
+ ##
+ # Render this widget or serve it from cache
+ def render_with_cache(_options = {}, &block)
+ if cached?
+ write Rails.cache.fetch(cache_key)
+ else
+ render(&block)
+ Rails.cache.write(cache_key, @cache_output || @output) if cache?
+ end
+ end
+
+ ##
+ # Set the canvas. If the canvas object isn't a string (e.g. cannot be cached easily),
+ # a @cache_output String is created, that will mirror what is being written to the canvas.
+ def set_canvas(canvas)
+ @cache_output = ''.html_safe
+ @output = canvas
+ end
+
+ ##
+ # Appends the Help Widget with this Widget's help text.
+ # If no help-text was given and no default help-text is set,
+ # the given default html will be printed instead.
+ # Params:
+ # - html
+ # - options-hash
+ # - :fallback_html (string, default: '') - the html code to render if no help-text was found
+ # - :help_text (string) - the help text to render
+ # - :instant_write (bool, default: true) - wether to write
+ # the help-widget instantly to the output-buffer.
+ # If set to false you should care to save the rendered text.
+ def maybe_with_help(options = {})
+ options[:instant_write] = true if options[:instant_write].nil?
+ options[:fallback_html] ||= ''
+ output = ''.html_safe
+ if text = options[:help_text] || help_text
+ output += render_widget Widget::Help, text do
+ options
+ end
+ else
+ output += options[:fallback_html]
+ end
+ write output if options[:instant_write]
+ output.html_safe
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls.rb b/vendored-plugins/reporting_engine/lib/widget/controls.rb
new file mode 100644
index 0000000000..389e8de5ad
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls.rb
@@ -0,0 +1,24 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls < Widget::Base
+ def cache_key
+ "#{super}#{@subject.new_record? ? 1 : 0}"
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/apply.rb b/vendored-plugins/reporting_engine/lib/widget/controls/apply.rb
new file mode 100644
index 0000000000..f42b9cd98e
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/apply.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::Apply < Widget::Controls
+ def render
+ write link_to(l(:button_apply),
+ '#',
+ id: 'query-icon-apply-button',
+ class: 'button -highlight',
+ :'data-target' => url_for(action: 'index', set_filter: '1'))
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/clear.rb b/vendored-plugins/reporting_engine/lib/widget/controls/clear.rb
new file mode 100644
index 0000000000..5e901c54f6
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/clear.rb
@@ -0,0 +1,28 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::Clear < Widget::Controls
+ def render
+ write link_to(l(:button_clear),
+ '#',
+ id: 'query-link-clear',
+ class: 'button icon-context icon-undo')
+ maybe_with_help
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/delete.rb b/vendored-plugins/reporting_engine/lib/widget/controls/delete.rb
new file mode 100644
index 0000000000..e72d649928
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/delete.rb
@@ -0,0 +1,45 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::Delete < Widget::Controls
+ def render
+ return '' if @subject.new_record? or !@options[:can_delete]
+ button = link_to(l(:button_delete),
+ '#',
+ id: 'query-icon-delete',
+ class: 'button icon-context icon-delete')
+ popup = content_tag :div, id: 'delete_form', style: 'display:none', class: 'button_form' do
+ question = content_tag :p, l(:label_really_delete_question)
+ options = content_tag :p do
+ delete_button = content_tag :span do
+ span = content_tag :em do
+ l(:button_delete)
+ end
+ end
+ url_opts = { id: @subject.id }
+ url_opts[request_forgery_protection_token] = form_authenticity_token # if protect_against_forgery?
+ opt1 = link_to delete_button, url_for(url_opts), method: :delete, class: 'button apply'
+ opt2 = link_to l(:button_cancel), '#', id: 'query-icon-delete-cancel', class: 'icon icon-cancel'
+ opt1 + opt2
+ end
+ question + options
+ end
+ write(button + popup)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/query_name.rb b/vendored-plugins/reporting_engine/lib/widget/controls/query_name.rb
new file mode 100644
index 0000000000..47f079a037
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/query_name.rb
@@ -0,0 +1,39 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::QueryName < Widget::Controls
+ dont_cache! # The name might change, but the query stays the same...
+
+ def render
+ options = { id: 'query_saved_name', 'data-translations' => translations }
+ if @subject.new_record?
+ name = l(:label_new_report)
+ icon = ''
+ else
+ name = @subject.name
+ options['data-is_public'] = @subject.public?
+ options['data-is_new'] = @subject.new_record?
+ end
+ write(content_tag(:span, h(name), options) + icon.to_s)
+ end
+
+ def translations
+ { isPublic: l(:field_is_public) }.to_json
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/save.rb b/vendored-plugins/reporting_engine/lib/widget/controls/save.rb
new file mode 100644
index 0000000000..ee7d846fc9
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/save.rb
@@ -0,0 +1,29 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::Save < Widget::Controls
+ def render
+ return '' if @subject.new_record? or !@options[:can_save]
+ write link_to(l(:button_save),
+ '#',
+ id: 'query-breadcrumb-save',
+ class: 'button icon-context icon-save',
+ :"data-target" => url_for(action: 'update', id: @subject.id, set_filter: '1'))
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/controls/save_as.rb b/vendored-plugins/reporting_engine/lib/widget/controls/save_as.rb
new file mode 100644
index 0000000000..0070a4b799
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/controls/save_as.rb
@@ -0,0 +1,79 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Controls::SaveAs < Widget::Controls
+ def render
+ if @subject.new_record?
+ link_name = l(:button_save)
+ icon = 'icon-save'
+ else
+ link_name = l(:button_save_as)
+ icon = 'icon-save'
+ end
+ button = link_to(link_name, '#', id: 'query-icon-save-as', class: "button icon-context #{icon}")
+ write(button + render_popup)
+ maybe_with_help
+ end
+
+ def cache_key
+ "#{super}#{@subject.name}"
+ end
+
+ def render_popup_form
+ name = content_tag :p, class: 'inline-label' do
+ label_tag(:query_name,
+ required_field_name(Query.human_attribute_name(:name)),
+ class: 'form-label -transparent') +
+ text_field_tag(:query_name,
+ @subject.name,
+ required: true)
+ end
+ if @options[:can_save_as_public]
+ box = content_tag :p, class: 'inline-label' do
+ label_tag(:query_is_public,
+ Query.human_attribute_name(:is_public),
+ class: 'form-label -transparent') +
+ check_box_tag(:query_is_public)
+ end
+ name + box
+ else
+ name
+ end
+ end
+
+ def render_popup_buttons
+ save = link_to(l(:button_save),
+ '#',
+ id: 'query-icon-save-button',
+ class: 'button -highlight icon-context icon-save',
+ :"data-target" => url_for(action: 'create', set_filter: '1'))
+
+ cancel = link_to(l(:button_cancel),
+ '#',
+ id: 'query-icon-save-as-cancel',
+ class: 'button icon-context icon-cancel')
+ save + cancel
+ end
+
+ def render_popup
+ content_tag :div, id: 'save_as_form', class: 'button_form', style: 'display:none' do
+ render_popup_form + render_popup_buttons
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters.rb b/vendored-plugins/reporting_engine/lib/widget/filters.rb
new file mode 100644
index 0000000000..0b85984cab
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters.rb
@@ -0,0 +1,116 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Filters < Widget::Base
+ def render
+ spacer = content_tag :li, '', class: 'advanced-filters--spacer'
+
+ add_filter = content_tag :li, id: 'add_filter_block', class: 'advanced-filters--add-filter' do
+ add_filter_label = label_tag 'add_filter_select', l(:label_filter_add),
+ class: 'advanced-filters--add-filter-label'
+
+ add_filter_value = content_tag :div, class: 'advanced-filters--add-filter-value' do
+ value = select_tag 'add_filter_select',
+ options_for_select([['', '']] + selectables),
+ class: 'advanced-filters--select',
+ name: nil
+ value += maybe_with_help icon: { class: 'filter-icon' },
+ tooltip: { class: 'filter-tip' },
+ instant_write: false # help associated with this kind of Widget
+
+ value
+ end
+
+ (add_filter_label + add_filter_value).html_safe
+ end
+
+ list = content_tag :ul, id: 'filter_table', class: 'advanced-filters--filters' do
+ render_filters + spacer + add_filter
+ end
+
+ write content_tag(:div, list)
+ end
+
+ def selectables
+ filters = engine::Filter.all
+ filters.sort_by(&:label).select(&:selectable?).map do |filter|
+ [filter.label, filter.underscore_name]
+ end
+ end
+
+ def render_filters
+ active_filters = @subject.filters.select(&:display?)
+ engine::Filter.all.select(&:selectable?).map do |filter|
+ opts = { id: "filter_#{filter.underscore_name}",
+ class: "#{filter.underscore_name} advanced-filters--filter",
+ :"data-filter-name" => filter.underscore_name }
+ active_instance = active_filters.detect { |f| f.class == filter }
+ if active_instance
+ opts[:"data-selected"] = true
+ else
+ opts[:style] = 'display:none'
+ end
+ content_tag :li, opts do
+ render_filter filter, active_instance
+ end
+ end.join.html_safe
+ end
+
+ def render_filter(f_cls, f_inst)
+ f = f_inst || f_cls
+ html = ''.html_safe
+ render_widget Label, f, to: html
+ render_widget Operators, f, to: html
+ if f_cls.heavy?
+ render_widget Heavy, f, to: html
+ elsif engine::Operator.string_operators.all? { |o| f_cls.available_operators.include? o }
+ render_widget TextBox, f, to: html
+ elsif engine::Operator.time_operators.all? { |o| f_cls.available_operators.include? o }
+ render_widget Date, f, to: html
+ elsif engine::Operator.integer_operators.all? { |o| f_cls.available_operators.include? o }
+ if f_cls.available_values.empty?
+ render_widget TextBox, f, to: html
+ else
+ render_widget MultiValues, f, to: html, lazy: true
+ end
+ else
+ if f_cls.is_multiple_choice?
+ render_widget MultiChoice, f, to: html
+ else
+ render_widget MultiValues, f, to: html, lazy: true
+ end
+ end
+ render_filter_help f, to: html
+ render_widget RemoveButton, f, to: html
+ end
+
+ # #Renders help for a filter (chainable)
+ def render_filter_help(filter, options = {})
+ html = content_tag :td, width: '25px' do
+ if filter.help_text # help associated with the chainable this Widget represents
+ render_widget Widget::Controls::Help, filter.help_text
+ end
+ end
+ if canvas = options[:to]
+ canvas << "\n" << html
+ else
+ html
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/base.rb b/vendored-plugins/reporting_engine/lib/widget/filters/base.rb
new file mode 100644
index 0000000000..c83e7c3988
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/base.rb
@@ -0,0 +1,33 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Filters::Base < Widget::Base
+ attr_reader :filter, :filter_class
+
+ def initialize(filter)
+ if filter.class == Class
+ @filter_class = filter
+ @filter = filter.new
+ else
+ @filter = filter
+ @filter_class = filter.class
+ end
+ @engine = filter.engine
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/date.rb b/vendored-plugins/reporting_engine/lib/widget/filters/date.rb
new file mode 100644
index 0000000000..3dd8d9159f
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/date.rb
@@ -0,0 +1,62 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::Date < Widget::Filters::Base
+ def render
+ @calendar_headers_tags_included = true
+
+ name = "values[#{filter_class.underscore_name}][]"
+ id_prefix = "#{filter_class.underscore_name}_"
+
+ write(content_tag(:span, class: 'advanced-filters--filter-value -binary') do
+ label1 = label_tag "#{id_prefix}arg_1_val",
+ h(filter_class.label) + ' ' + l(:label_filter_value),
+ class: 'hidden-for-sighted'
+
+ arg1 = content_tag :span, id: "#{id_prefix}arg_1" do
+ text1 = text_field_tag name, @filter.values.first.to_s,
+ size: 10,
+ class: 'advanced-filters--text-field',
+ id: "#{id_prefix}arg_1_val",
+ :'data-type' => 'date'
+ cal1 = calendar_for("#{id_prefix}arg_1_val")
+ label1 + text1 + cal1
+ end
+
+ label2 = label_tag "#{id_prefix}arg_2_val",
+ h(filter_class.label) + ' ' + l(:label_filter_value),
+ class: 'hidden-for-sighted'
+
+ arg2 = content_tag :span, id: "#{id_prefix}arg_2", class: 'advanced-filters--filter-value2' do
+ text2 = text_field_tag "#{name}", @filter.values.second.to_s,
+ size: 10,
+ class: 'advanced-filters--text-field',
+ id: "#{id_prefix}arg_2_val",
+ :'data-type' => 'date'
+ cal2 = calendar_for "#{id_prefix}arg_2_val"
+ label2 + text2 + cal2
+ end
+
+ arg1 + arg2
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/heavy.rb b/vendored-plugins/reporting_engine/lib/widget/filters/heavy.rb
new file mode 100644
index 0000000000..5764363683
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/heavy.rb
@@ -0,0 +1,65 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# FIXME: This basically is the MultiValues-Filter, except that we do not show
+# The select-box. This way we allow our JS to pretend this is just another
+# Filter. This is overhead...
+# But well this is again one of those temporary solutions.
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::Heavy < Widget::Filters::Base
+ def render
+ # TODO: sometimes filter.values is of the form [["3"]] and somtimes ["3"].
+ # (using cost reporting)
+ # this might be a bug - further research would be fine
+ values = filter.values.first.is_a?(Array) ? filter.values.first : filter.values
+ opts = Array(values).empty? ? [] : values.map { |i| filter_class.label_for_value(i.to_i) }
+ div = content_tag :div, id: "#{filter_class.underscore_name}_arg_1", class: 'advanced-filters--filter-value hidden' do
+ select_options = { :"data-remote-url" => url_for(action: 'available_values'),
+ name: "values[#{filter_class.underscore_name}][]",
+ :"data-loading" => '',
+ id: "#{filter_class.underscore_name}_arg_1_val",
+ class: 'advanced-filters--select filter-value',
+ :"data-filter-name" => filter_class.underscore_name,
+ multiple: 'multiple' }
+ # multiple will be disabled/enabled later by JavaScript anyhow.
+ # We need to specify multiple here because of an IE6-bug.
+ if filter_class.has_dependent?
+ all_dependents = filter_class.all_dependents.map(&:underscore_name).to_json
+ select_options.merge! :"data-all-dependents" => all_dependents.gsub!('"', "'")
+ next_dependents = filter_class.dependents.map(&:underscore_name).to_json
+ select_options.merge! :"data-next-dependents" => next_dependents.gsub!('"', "'")
+ end
+ # store selected value(s) in data-initially-selected if this filter is a dependent
+ # of another filter, as we have to restore values manually in the client js
+ if (filter_class.is_dependent? || @options[:lazy]) && !Array(filter.values).empty?
+ select_options.merge! :"data-initially-selected" => filter.values.to_json.gsub!('"', "'")
+ end
+ box = content_tag :select, select_options do
+ render_widget Widget::Filters::Option, filter, to: '', content: opts
+ end
+ box
+ end
+ alternate_text = opts.map(&:first).join(', ').html_safe
+ write(div + content_tag(:label) do
+ alternate_text
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/label.rb b/vendored-plugins/reporting_engine/lib/widget/filters/label.rb
new file mode 100644
index 0000000000..c4d1807b81
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/label.rb
@@ -0,0 +1,36 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::Label < Widget::Filters::Base
+ def render
+ options = {
+ id: filter_class.underscore_name,
+ class: 'advanced-filters--filter-name'
+ }
+ if engine::Filter.all.any? { |f| f.dependents.include?(filter_class) }
+ options.merge! class: 'dependent-filter-label'
+ end
+ write(content_tag(:label, options) do
+ h(filter_class.label)
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/multi_choice.rb b/vendored-plugins/reporting_engine/lib/widget/filters/multi_choice.rb
new file mode 100644
index 0000000000..a54dd4d378
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/multi_choice.rb
@@ -0,0 +1,55 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::MultiChoice < Widget::Filters::Base
+ def render
+ filterName = filter_class.underscore_name
+ write(content_tag :div, id: "#{filterName}_arg_1", class: 'advanced-filters--filter-value' do
+ choices = filter_class.available_values.each_with_index.map do |(label, value), i|
+ opts = {
+ type: 'radio',
+ name: "values[#{filterName}][]",
+ id: "#{filterName}_radio_option_#{i}",
+ value: value
+ }
+ opts[:checked] = 'checked' if filter.values == [value].flatten
+ radio_button = tag :input, opts
+ content_tag :label, radio_button + translate(label),
+ for: "#{filterName}_radio_option_#{i}",
+ :'data-filter-name' => filter_class.underscore_name,
+ class: "#{filterName}_radio_option filter_radio_option"
+ end
+ content_tag :div, choices.join.html_safe,
+ id: "#{filter_class.underscore_name}_arg_1_val"
+ end)
+ end
+
+ private
+
+ def translate(label)
+ if label.is_a?(Symbol)
+ ::I18n.t(label)
+ else
+ label
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/multi_values.rb b/vendored-plugins/reporting_engine/lib/widget/filters/multi_values.rb
new file mode 100644
index 0000000000..8636bb1362
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/multi_values.rb
@@ -0,0 +1,70 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::MultiValues < Widget::Filters::Base
+ def render
+ write(content_tag(:div, id: "#{filter_class.underscore_name}_arg_1", class: 'advanced-filters--filter-value') do
+ select_options = { :"data-remote-url" => url_for(action: 'available_values'),
+ style: 'vertical-align: top;', # FIXME: Do CSS
+ name: "values[#{filter_class.underscore_name}][]",
+ :"data-loading" => @options[:lazy] ? 'ajax' : '',
+ id: "#{filter_class.underscore_name}_arg_1_val",
+ class: 'advanced-filters--select filter-value',
+ :"data-filter-name" => filter_class.underscore_name,
+ multiple: 'multiple' }
+ # multiple will be disabled/enabled later by JavaScript anyhow.
+ # We need to specify multiple here because of an IE6-bug.
+ if filter_class.has_dependent?
+ all_dependents = filter_class.all_dependents.map(&:underscore_name).to_json
+ select_options.merge! :"data-all-dependents" => all_dependents.gsub!('"', "'")
+ next_dependents = filter_class.dependents.map(&:underscore_name).to_json
+ select_options.merge! :"data-next-dependents" => next_dependents.gsub!('"', "'")
+ end
+ # store selected value(s) in data-initially-selected if this filter is a dependent
+ # of another filter, as we have to restore values manually in the client js
+ if (filter_class.is_dependent? || @options[:lazy]) && !Array(filter.values).empty?
+ select_options.merge! :"data-initially-selected" =>
+ filter.values.to_json.gsub!('"', "'") || '[' + filter.values.map { |v| "'#{v}'" }.join(',') + ']'
+ end
+ select_options.merge! :"data-dependent" => true if filter_class.is_dependent?
+ box_content = ''.html_safe
+ label = label_tag "#{filter_class.underscore_name}_arg_1_val",
+ h(filter_class.label) + ' ' + l(:label_filter_value),
+ class: 'hidden-for-sighted'
+
+ box = content_tag :select, select_options, id: "#{filter_class.underscore_name}_select_1" do
+ render_widget Widget::Filters::Option, filter, to: box_content unless @options[:lazy]
+ end
+ plus = content_tag :a, href: 'javascript:', class: 'form-label filter_multi-select -transparent',
+ :"data-filter-name" => filter_class.underscore_name,
+ title: l(:description_multi_select) do
+ content_tag :span, '', class: 'icon-context icon-button icon-add icon4', title: l(:label_enable_multi_select) do
+ content_tag :span, l(:label_enable_multi_select), class: 'hidden-for-sighted'
+ end
+ end
+
+ content_tag(:span, class: 'inline-label') do
+ label + box + plus
+ end
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/operators.rb b/vendored-plugins/reporting_engine/lib/widget/filters/operators.rb
new file mode 100644
index 0000000000..b58841bb41
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/operators.rb
@@ -0,0 +1,53 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::Operators < Widget::Filters::Base
+ def render
+ write(content_tag(:div, class: 'advanced-filters--filter-operator') do
+ hide_select_box = (filter_class.available_operators.count == 1 || filter_class.heavy?)
+ options = { class: 'advanced-filters--select filters-select filter_operator',
+ id: "operators[#{filter_class.underscore_name}]",
+ name: "operators[#{filter_class.underscore_name}]",
+ :"data-filter-name" => filter_class.underscore_name }
+ options.merge! style: 'display: none' if hide_select_box
+
+ select_box = content_tag :select, options do
+ filter_class.available_operators.map do |o|
+ opts = { value: h(o.to_s), :"data-arity" => o.arity }
+ opts.reverse_merge! :"data-forced" => o.forced if o.forced?
+ opts[:selected] = 'selected' if filter.operator.to_s == o.to_s
+ content_tag(:option, opts) { h(I18n.t(o.label)) }
+ end.join.html_safe
+ end
+ label1 = content_tag :label,
+ h(filter_class.label) + ' ' + l(:label_operator),
+ for: "operators[#{filter_class.underscore_name}]",
+ class: 'hidden-for-sighted'
+ label = content_tag :label do
+ if filter_class.available_operators.any?
+ filter_class.available_operators.first.label
+ end
+ end
+ hide_select_box ? label1 + select_box + label : label1 + select_box
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/option.rb b/vendored-plugins/reporting_engine/lib/widget/filters/option.rb
new file mode 100644
index 0000000000..0489f5bb35
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/option.rb
@@ -0,0 +1,48 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+##
+# Accepts option :content, which expects an enumerable of [name, id, *args]
+# as it would appear in a filters available values. If given, it renders the
+# option-tags from the content array instead of the filters available values.
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::Option < Widget::Filters::Base
+ def render
+ first = true
+ write((@options[:content] || filter_class.available_values).map do |name, id, *args|
+ options = args.first || {} # optional configuration for values
+ level = options[:level] # nesting_level is optional for values
+ name = l(name) if name.is_a? Symbol
+ name = name.empty? ? l(:label_none) : name
+ name_prefix = ((level && level > 0) ? (' ' * 2 * level + '> ') : '')
+ unless options[:optgroup]
+ opts = { value: id }
+ if (Array(filter.values).map(&:to_s).include? id.to_s) || (first && Array(filter.values).empty?)
+ opts[:selected] = 'selected'
+ end
+ first = false
+ content_tag(:option, opts) { name_prefix + name }
+ else
+ tag :optgroup, label: l(:label_sector)
+ end
+ end.join.html_safe)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/remove_button.rb b/vendored-plugins/reporting_engine/lib/widget/filters/remove_button.rb
new file mode 100644
index 0000000000..71fcc28048
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/remove_button.rb
@@ -0,0 +1,34 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::RemoveButton < Widget::Filters::Base
+ def render
+ hidden_field = tag :input, id: "rm_#{filter_class.underscore_name}",
+ name: 'fields[]', type: 'hidden', value: ''
+ button = content_tag(:a, href: "#", class: "filter_rem") do
+ icon_wrapper('icon-context advanced-filters--remove-filter-icon', l(:description_remove_filter))
+ end
+
+ write(content_tag(:div, hidden_field + button, id: "rm_box_#{filter_class.underscore_name}",
+ class: 'advanced-filters--remove-filter'))
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/filters/text_box.rb b/vendored-plugins/reporting_engine/lib/widget/filters/text_box.rb
new file mode 100644
index 0000000000..8ad2060504
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/filters/text_box.rb
@@ -0,0 +1,38 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+# make sure to require Widget::Filters::Base first because otherwise
+# ruby might find Base within Widget and Rails will not load it
+require_dependency 'widget/filters/base'
+class Widget::Filters::TextBox < Widget::Filters::Base
+ def render
+ label = content_tag :label,
+ "#{h(filter_class.label)} #{l(:label_filter_value)}",
+ for: "#{filter_class.underscore_name}_arg_1_val",
+ class: 'hidden-for-sighted'
+
+ write(content_tag(:div, id: "#{filter_class.underscore_name}_arg_1", class: 'advanced-filters--filter-value') do
+ label + text_field_tag("values[#{filter_class.underscore_name}]", '',
+ size: '6',
+ class: 'advanced-filters--text-field',
+ id: "#{filter_class.underscore_name}_arg_1_val",
+ :'data-filter-name' => filter_class.underscore_name)
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/group_bys.rb b/vendored-plugins/reporting_engine/lib/widget/group_bys.rb
new file mode 100644
index 0000000000..bb531a7869
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/group_bys.rb
@@ -0,0 +1,90 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::GroupBys < Widget::Base
+ def render_options(group_by_ary)
+ group_by_ary.sort_by(&:label).map do |group_by|
+ next unless group_by.selectable?
+ content_tag :option, value: group_by.underscore_name, :'data-label' => "#{CGI::escapeHTML(h(group_by.label))}" do
+ h(group_by.label)
+ end
+ end.join.html_safe
+ end
+
+ def render_group_caption(_type)
+ content_tag :span do
+ out = content_tag :span, class: 'arrow in_row arrow_group_by_caption' do
+ '' # cannot use tag here as it would generate which leads to wrong interpretation in most browsers
+ end
+ out.html_safe
+ end
+ end
+
+ def render_group(type, initially_selected, show_help = false)
+ initially_selected = initially_selected.map do |group_by|
+ [group_by.class.underscore_name, h(group_by.class.label)]
+ end
+
+ content_tag :fieldset,
+ id: "group_by_#{type}",
+ class: 'drag_target drag_container',
+ :'data-initially-selected' => initially_selected.to_json.gsub('"', "'") do
+ out = content_tag :legend, l(:"label_#{type}"), class: 'in_row group_by_caption'
+
+ out += render_group_caption type
+
+ out += label_tag "add_group_by_#{type}",
+ l(:"label_group_by_add"),
+ class: 'hidden-for-sighted'
+
+ out += content_tag :select, id: "add_group_by_#{type}", class: 'advanced-filters--select' do
+ content = content_tag :option, "-- #{l(:label_group_by_add)} --", value: ''
+
+ content += engine::GroupBy.all_grouped.sort_by do |label, _group_by_ary|
+ l(label)
+ end.map do |label, group_by_ary|
+ content_tag :optgroup, label: h(l(label)) do
+ render_options group_by_ary
+ end
+ end.join.html_safe
+ content
+ end
+
+ if show_help
+ out += maybe_with_help icon: { class: 'group-by-icon' },
+ tooltip: { class: 'group-by-tip' },
+ instant_write: false
+ end
+
+ out
+ end
+ end
+
+ def render
+ write(content_tag(:div, id: 'group_by_area') do
+ out = render_group 'columns', @subject.group_bys(:column), true
+ out += render_group 'rows', @subject.group_bys(:row)
+ out += image_tag 'reporting_engine/remove.gif',
+ id: 'hidden_remove_img',
+ style: 'display:none',
+ class: 'reporting_hidden_group_remove_image'
+ out.html_safe
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/help.rb b/vendored-plugins/reporting_engine/lib/widget/help.rb
new file mode 100644
index 0000000000..53ec006a67
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/help.rb
@@ -0,0 +1,76 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+##
+# Usage: render_widget Widget::Help, :text
+#
+# Where :text is a i18n key.
+class Widget::Help < Widget::Base
+ dont_cache!
+
+ def render
+ id = "tip:#{@subject}"
+ options = { icon: {}, tooltip: {} }
+ options.merge!(yield) if block_given?
+ sai = options[:show_at_id] ? ", show_at_id: '#{options[:show_at_id]}'" : ''
+
+ icon = tag :img, src: image_path('reporting_engine/icon_info_red.gif'), id: "target:#{@subject}", alt: ''
+ tip = content_tag_string :span, l(@subject), tip_config(options[:tooltip]), false
+ script = content_tag :script,
+ "new Tooltip('target:#{@subject}', 'tip:#{@subject}', {className: 'tooltip'#{sai}});",
+ { type: 'text/javascript' }, false
+ target = content_tag :a, icon + tip, icon_config(options[:icon])
+ write(target + script)
+ end
+
+ def icon_config(options)
+ add_class = lambda do |cl|
+ if cl
+ "help #{cl}"
+ else
+ 'help'
+ end
+ end
+ options.mega_merge! href: '#', class: add_class
+ end
+
+ def tip_config(options)
+ add_class = lambda do |cl|
+ if cl
+ "#{cl} tooltip"
+ else
+ 'tooltip'
+ end
+ end
+ options.mega_merge! id: "tip:#{@subject}", class: add_class
+ end
+end
+
+class Hash
+ def mega_merge!(hash)
+ hash.each do |key, value|
+ if value.is_a?(Proc)
+ self[key] = value.call(self[key])
+ else
+ self[key] = value
+ end
+ end
+ self
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/settings.rb b/vendored-plugins/reporting_engine/lib/widget/settings.rb
new file mode 100644
index 0000000000..2e81c22fb6
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/settings.rb
@@ -0,0 +1,86 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Settings < Widget::Base
+ dont_cache! # Settings may change due to permissions
+
+ @@settings_to_render = [:filter, :group_by, :controls]
+
+ def render_filter_settings
+ render_widget Widget::Settings::Fieldset, @subject,
+ type: 'filters', help_text: filter_help do
+ render_widget Widget::Filters, @subject
+ end
+ end
+
+ def render_group_by_settings
+ render_widget Widget::Settings::Fieldset, @subject,
+ type: 'group_by', help_text: group_by_help do
+ render_widget Widget::GroupBys, @subject
+ end
+ end
+
+ def render_controls_settings
+ content_tag :div, class: 'buttons form_controls' do
+ widgets = ''.html_safe
+ render_widget(Widget::Controls::Apply, @subject, to: widgets)
+ render_widget(Widget::Controls::Save, @subject, to: widgets,
+ can_save: allowed_to?(:save, @subject, current_user))
+ if allowed_to?(:create, @subject, current_user)
+ render_widget(Widget::Controls::SaveAs, @subject, to: widgets,
+ can_save_as_public: allowed_to?(:save_as_public, @subject, current_user))
+ end
+ render_widget(Widget::Controls::Clear, @subject, to: widgets)
+ render_widget(Widget::Controls::Delete, @subject, to: widgets,
+ can_delete: allowed_to?(:delete, @subject, current_user))
+ end
+ end
+
+ def render
+ write(form_tag('#', id: 'query_form', method: :post) do
+ content_tag :div, id: 'query_form_content' do
+ # will render a setting menu for every setting.
+ # To add new settings, write a new instance method render__setting
+ # and add to the @@settings_to_render list.
+ content = ''.html_safe
+ @@settings_to_render.each do |setting_name|
+ render_method_name = "render_#{setting_name}_settings"
+ content << send(render_method_name) if respond_to? render_method_name
+ end
+ content
+ end
+ end)
+ end
+
+ def filter_help
+ if help_text.is_a?(Array)
+ help_text[0]
+ else
+ nil
+ end
+ end
+
+ def group_by_help
+ if help_text.is_a?(Array)
+ help_text[1]
+ else
+ nil
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/settings/fieldset.rb b/vendored-plugins/reporting_engine/lib/widget/settings/fieldset.rb
new file mode 100644
index 0000000000..75f9ad2a07
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/settings/fieldset.rb
@@ -0,0 +1,47 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Settings::Fieldset < Widget::Base
+ dont_cache!
+
+ def render_with_options(options, &block)
+ @type = options.delete(:type) || 'filter'
+ @id = "#{@type}"
+ @label = :"label_#{@type}"
+ super(options, &block)
+ end
+
+ def render
+ hash = self.hash
+ write(content_tag(:fieldset,
+ id: @id,
+ class: 'form--fieldset -collapsible') do
+ html = content_tag(:legend,
+ show_at_id: hash.to_s,
+ icon: "#{@type}-legend-icon",
+ tooltip: "#{@type}-legend-tip",
+ onclick: 'toggleFieldset(this);',
+ class: 'form--fieldset-legend',
+ id: hash.to_s) do # FIXME: onclick
+ content_tag(:a, href: 'javascript:') do (l(@label) + maybe_with_help(instant_write: false)).html_safe end
+ end
+ html + yield
+ end)
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/table.rb b/vendored-plugins/reporting_engine/lib/widget/table.rb
new file mode 100644
index 0000000000..f3c03ae83b
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/table.rb
@@ -0,0 +1,68 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table < Widget::Base
+ extend Report::InheritedAttribute
+ include ReportingHelper
+
+ attr_accessor :debug
+ attr_accessor :fields
+ attr_accessor :mapping
+
+ def initialize(query)
+ raise ArgumentError, 'Tables only work on Reports!' unless query.is_a? Report
+ super
+ end
+
+ def resolve_table
+ if @subject.group_bys.size == 0
+ self.class.detailed_table
+ elsif @subject.group_bys.size == 1
+ self.class.simple_table
+ else
+ self.class.fancy_table
+ end
+ end
+
+ def self.detailed_table(klass = nil)
+ @@detail_table = klass if klass
+ defined?(@@detail_table) ? @@detail_table : fancy_table
+ end
+
+ def self.simple_table(klass = nil)
+ @@simple_table = klass if klass
+ defined?(@@simple_table) ? @@simple_table : fancy_table
+ end
+
+ def self.fancy_table(klass = nil)
+ @@fancy_table = klass if klass
+ @@fancy_table
+ end
+ fancy_table Widget::Table::ReportTable
+
+ def render
+ write('')
+ if @subject.result.count <= 0
+ write(content_tag(:p, l(:label_no_data), class: 'nodata'))
+ else
+ str = render_widget(resolve_table, @subject, @options.reverse_merge(to: @output))
+ @cache_output.write(str.html_safe) if @cache_output
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/table/progressbar.rb b/vendored-plugins/reporting_engine/lib/widget/table/progressbar.rb
new file mode 100644
index 0000000000..1bdf7dc680
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/table/progressbar.rb
@@ -0,0 +1,50 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table::Progressbar < Widget::Base
+ dont_cache!
+
+ # Defines the minimum number of cells for a 'big' report
+ # Big reports may be handled differently in the UI - i.e. ask the user
+ # if he's really sure to execute such a heavy report
+ THRESHHOLD = 2000
+
+ def render
+ if render_table?
+ render_widget Widget::Table, @subject, to: (@output ||= ''.html_safe)
+ else
+ write(content_tag(:label, style: 'display:none') do
+ content_tag(:div, l(:label_progress_bar_explanation).html_safe) + render_progress_bar
+ end)
+ end
+ end
+
+ def render_table?
+ Widget::Table.new(@subject).resolve_table.new(@subject).cached? || @subject.size <= THRESHHOLD
+ end
+
+ def render_progress_bar
+ content_tag(:div, '',
+ id: 'progressbar',
+ class: 'form_controls',
+ :"data-query-size" => @subject.size,
+ :"data-translation" => ::I18n.translate(:label_load_query_question, size: @subject.size),
+ :"data-target" => url_for(action: 'index', set_filter: '1', immediately: true))
+ end
+end
diff --git a/vendored-plugins/reporting_engine/lib/widget/table/report_table.rb b/vendored-plugins/reporting_engine/lib/widget/table/report_table.rb
new file mode 100644
index 0000000000..80c348ec68
--- /dev/null
+++ b/vendored-plugins/reporting_engine/lib/widget/table/report_table.rb
@@ -0,0 +1,178 @@
+#-- copyright
+# ReportingEngine
+#
+# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#++
+
+class Widget::Table::ReportTable < Widget::Table
+ attr_accessor :walker
+
+ def configure_query
+ if @subject.depth_of(:row) == 0
+ @subject.row(:singleton_value)
+ elsif @subject.depth_of(:column) == 0
+ @subject.column(:singleton_value)
+ end
+ end
+
+ def configure_walker
+ @walker ||= @subject.walker
+ @walker.for_final_row do |row, cells|
+ html = " #{show_row row}#{debug_fields(row)} "
+ html << cells.join
+ html << "#{show_result(row)}#{debug_fields(row)}"
+ html.html_safe
+ end
+
+ @walker.for_row do |row, subrows|
+ subrows.flatten!
+ unless row.fields.empty?
+ subrows[0] = %{
+ #{show_row row}#{debug_fields(row)}
+ #{subrows[0].gsub("class='normal", "class='top")}
+ #{show_result(row)}#{debug_fields(row)}
+ }.html_safe
+ end
+ subrows.last.gsub!("class='normal", "class='bottom")
+ subrows.last.gsub!("class='top", "class='bottom top")
+ subrows
+ end
+
+ @walker.for_empty_cell { " ".html_safe }
+
+ @walker.for_cell do |result|
+ write(' '.html_safe) # XXX: This keeps the Apache from timing out on us. Keep-Alive byte!
+ "#{show_result result}#{debug_fields(result)} ".html_safe
+ end
+ end
+
+ def render
+ configure_query
+ configure_walker
+ write ""
+ render_thead
+ render_tfoot
+ render_tbody
+ write '
'
+ end
+
+ def render_tbody
+ write ''
+ first = true
+ odd = true
+ walker.body do |line|
+ if first
+ line.gsub!("class='normal", "class='top")
+ first = false
+ end
+ mark_penultimate_column! line
+ write "#{line} "
+ odd = !odd
+ end
+ write ' '
+ end
+
+ def mark_penultimate_column!(line)
+ line.gsub! /()[^<]* '
+ walker.headers do |list, first, first_in_col, last_in_col|
+ write ' ' if first_in_col
+ if first
+ write(content_tag(:th, rowspan: @subject.depth_of(:column), colspan: @subject.depth_of(:row)) do
+ ''
+ end)
+ end
+ list.each do |column|
+ opts = { colspan: column.final_number(:column) }
+ opts.merge!(class: 'inner') if column.final?(:column)
+ write(content_tag(:th, opts) do
+ show_row column
+ end)
+ end
+ if first
+ write(content_tag(:th, rowspan: @subject.depth_of(:column), colspan: @subject.depth_of(:row)) do
+ ''
+ end)
+ end
+ write ' ' if last_in_col
+ end
+ write ''
+ end
+
+ def render_tfoot
+ return if walker.headers_empty?
+ write ''
+ walker.reverse_headers do |list, first, first_in_col, last_in_col|
+ if first_in_col
+ write ''
+ if first
+ write(content_tag(:th, rowspan: @subject.depth_of(:column), colspan: @subject.depth_of(:row), class: 'top') do
+ ' '
+ end)
+ end
+ end
+
+ list.each do |column|
+ opts = { colspan: column.final_number(:column) }
+ opts.merge!(class: 'inner') if first
+ write(content_tag(:th, opts) do
+ show_result(column) # {debug_fields(column)}
+ end)
+ end
+ if last_in_col
+ if first
+ write(content_tag(:th,
+ rowspan: @subject.depth_of(:column),
+ colspan: @subject.depth_of(:row),
+ class: 'top result') do
+ show_result @subject
+ end)
+ end
+ write ' '
+ end
+ end
+ write ' '
+ end
+
+ def debug_content
+ content_tag :pre do
+ debug_pre_content = '[ Query ]' +
+ @subject.chain.each do |child|
+ "#{h child.class.inspect}, #{h child.type}"
+ end
+
+ debug_pre_content += '[ RESULT ]'
+ @subject.result.recursive_each_with_level do |level, result|
+ debug_pre_content += '>>> ' * (level + 1)
+ debug_pre_content += h(result.inspect)
+ debug_pre_content += ' ' * (level + 1)
+ debug_pre_content += h(result.type.inspect)
+ debug_pre_content += ' ' * (level + 1)
+ debug_pre_content += h(result.fields.inspect)
+ end
+ debug_pre_content += '[ HEADER STACK ]'
+ debug_pre_content += walker.header_stack.each do |l|
+ ">>> #{l.inspect}"
+ end
+ end
+ end
+end
diff --git a/vendored-plugins/reporting_engine/package.json b/vendored-plugins/reporting_engine/package.json
new file mode 100644
index 0000000000..d075acd94a
--- /dev/null
+++ b/vendored-plugins/reporting_engine/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "reporting_engine",
+ "version": "0.1.0",
+ "main": "frontend/app/reporting_engine-app.js",
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/vendored-plugins/reporting_engine/reporting_engine.gemspec b/vendored-plugins/reporting_engine/reporting_engine.gemspec
new file mode 100644
index 0000000000..ae66a592d9
--- /dev/null
+++ b/vendored-plugins/reporting_engine/reporting_engine.gemspec
@@ -0,0 +1,21 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "reporting_engine/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "reporting_engine"
+ s.version = ReportingEngine::VERSION
+ s.authors = "Finn GmbH"
+ s.email = "info@finn.de"
+ s.homepage = "https://www.openproject.org/projects/plugin-reportingengine"
+ s.summary = "A Rails engine to create custom database reports"
+ s.description = "This Rails engine adds classes to create custom database reports with filtering and grouping functionality."
+ s.license = "GPLv3"
+
+ s.files = Dir["{config, doc, lib}/**/*", "README.md"]
+
+ s.add_dependency 'rails', '~> 4.2.4'
+ s.add_dependency "json"
+end
diff --git a/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/cordinc_tooltip.js b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/cordinc_tooltip.js
new file mode 100644
index 0000000000..c4c2ee260c
--- /dev/null
+++ b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/cordinc_tooltip.js
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2009 Charles Cordingley (www.cordinc.com)
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * cordinc_tooltip.js, v1.0.2 - 27 August 2008
+ * For help see www.cordinc.com/projects/tooltips.html
+ */
+var Tooltip = Class.create({
+ initialize: function(target, tooltip) {
+ var options = Object.extend({
+ start_effect: function(element) {},
+ end_effect: function(element) {},
+ zindex: 1000,
+ offset: {x:0, y:0},
+ hook: {target:'topRight', tip:'bottomLeft'},
+ trigger: false,
+ DOM_location: false,
+ className: false,
+ delay: {}
+ }, arguments[2] || {});
+ this.target = $(target);
+ this.show_at = (options.show_at_id !== undefined) ? $(options.show_at_id) : undefined
+ this.tooltip = $(tooltip);
+ this.options = options;
+ this.event_target = this.options.trigger?$(this.options.trigger):this.target;
+
+ if (this.options.className) {
+ this.tooltip.addClassName(this.options.className);
+ }
+ this.tooltip.hide();
+ this.display=false;
+
+ this.mouse_over = this.displayTooltip.bindAsEventListener(this);
+ this.mouse_out = this.removeTooltip.bindAsEventListener(this);
+ this.event_target.observe("mouseover", this.mouse_over);
+ this.event_target.observe("mouseout", this.mouse_out);
+ },
+
+ displayTooltip: function(event){
+ event.stop();
+
+ if (this.display) {return;}
+ if (this.options.delay.start) {
+ var self = this;
+ this.timer_id = setTimeout(function(){self.timer_id = false; self.showTooltip(event);}, this.options.delay.start*1000);
+ } else {
+ this.showTooltip(event);
+ }
+ },
+
+ showTooltip: function(event) {
+ var show_at = (this.show_at !== undefined) ? this.show_at : this.target
+ this.display=true;
+ position = this.positionTooltip(event);
+
+ this.clone = this.tooltip.cloneNode(true);
+ parentId = this.options.DOM_location?$(this.options.DOM_location.parentId):show_at.parentNode;
+ successorId = this.options.DOM_location?$(this.options.DOM_location.successorId):show_at;
+ parentId.insertBefore(this.clone, successorId);
+
+ this.clone.setStyle({
+ position: 'absolute',
+ top: position.top + "px",
+ left: position.left + "px",
+ display: "inline",
+ zIndex:this.options.zindex,
+ /* fix for ur dashboard */
+ visibility: 'visible',
+ width: "400px"
+ });
+
+ if (this.options.start_effect) {
+ this.options.start_effect(this.clone);
+ }
+ },
+
+ positionTooltip: function(event) {
+ target_position = this.target.cumulativeOffset();
+
+ tooltip_dimensions = this.tooltip.getDimensions();
+ target_dimensions = this.target.getDimensions();
+
+ this.positionModify(target_position, target_dimensions, this.options.hook.target, 1);
+ this.positionModify(target_position, tooltip_dimensions, this.options.hook.tip, -1);
+
+ target_position.top += this.options.offset.y;
+ target_position.left += this.options.offset.x;
+
+ return target_position;
+ },
+
+ positionModify: function(position, box, corner, neg) {
+ if (corner == 'topRight') {
+ position.left += box.width*neg;
+ } else if (corner == 'topLeft') {
+ } else if (corner == 'bottomLeft') {
+ position.top += box.height*neg;
+ } else if (corner == 'bottomRight') {
+ position.top += box.height*neg;
+ position.left += box.width*neg;
+ } else if (corner == 'topMid') {
+ position.left += (box.width/2)*neg;
+ } else if (corner == 'leftMid') {
+ position.top += (box.height/2)*neg;
+ } else if (corner == 'bottomMid') {
+ position.top += box.height*neg;
+ position.left += (box.width/2)*neg;
+ } else if (corner == 'rightMid') {
+ position.top += (box.height/2)*neg;
+ position.left += box.width*neg;
+ }
+ },
+
+ removeTooltip: function(event) {
+ if (this.timer_id) {
+ clearTimeout(this.timer_id);
+ this.timer_id = false;
+ return;
+ }
+
+ if (this.options.end_effect) {
+ this.options.end_effect(this.clone);
+ }
+
+ if (this.options.delay.end) {
+ var self = this;
+ setTimeout(function(){self.clearTooltip();}, this.options.delay.end*1000);
+ } else {
+ this.clearTooltip();
+ }
+ },
+
+ clearTooltip: function() {
+ if (this.clone !== undefined && this.clone !== null) {
+ this.clone.remove();
+ this.clone = null;
+ this.display=false;
+ }
+ },
+
+ destroy: function() {
+ this.event_target.stopObserving("mouseover", this.mouse_over);
+ this.event_target.stopObserving("mouseout", this.mouse_out);
+ this.clearTooltip();
+ }
+})
\ No newline at end of file
diff --git a/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/reporting/prototype_progress_bar.js b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/reporting/prototype_progress_bar.js
new file mode 100644
index 0000000000..68195af759
--- /dev/null
+++ b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/reporting/prototype_progress_bar.js
@@ -0,0 +1,116 @@
+/**
+ * @author Ryan Johnson
+ * @copyright 2008 PersonalGrid Corporation
+ * @package LivePipe UI
+ * @license MIT
+ * @url http://livepipe.net/control/progressbar
+ * @require prototype.js, livepipe.js
+ */
+
+/*global document, Prototype, Ajax, Class, PeriodicalExecuter, $, $A, Control */
+
+if (typeof(Prototype) === "undefined") {
+ throw "Control.ProgressBar requires Prototype to be loaded.";
+}
+if (typeof(Event) === "undefined") {
+ throw "Control.ProgressBar requires Event to be loaded.";
+}
+if (typeof(Control) === "undefined") {
+ if (window.console && window.console.log) {
+ window.console.log("Control.ProgressBar requires Prototype ~= 1.7");
+ }
+} else {
+
+Control.ProgressBar = Class.create({
+ initialize: function(container, options) {
+ this.progress = 0;
+ this.executer = false;
+ this.active = false;
+ this.poller = false;
+ this.container = $(container);
+ this.containerWidth = this.container.getDimensions().width;
+ this.progressContainer = $(document.createElement('div'));
+ this.progressContainer.setStyle({
+ width: this.containerWidth + 'px',
+ height: '100%',
+ position: 'absolute',
+ top: '0px',
+ right: '0px'
+ });
+ this.container.appendChild(this.progressContainer);
+ this.options = {
+ afterChange: Prototype.emptyFunction,
+ interval: 0.25,
+ step: 1,
+ classNames: {
+ active: 'progress_bar_active',
+ inactive: 'progress_bar_inactive'
+ }
+ };
+ Object.extend(this.options, options || {});
+ this.container.addClassName(this.options.classNames.inactive);
+ this.active = false;
+ },
+ setProgress: function (value) {
+ this.progress = value;
+ this.draw();
+ if (this.progress >= 100) {
+ this.stop(false);
+ }
+ this.notify('afterChange', this.progress, this.active);
+ },
+ poll: function (url, interval, ajaxOptions) {
+ // Extend the passed ajax options and success callback with our own.
+ ajaxOptions = ajaxOptions || {};
+ var success = ajaxOptions.onSuccess || Prototype.emptyFunction;
+ ajaxOptions.onSuccess = success.wrap(function (callOriginal, request) {
+ this.setProgress(parseInt(request.responseText, 10));
+ if (!this.active) {
+ this.poller.stop();
+ }
+ callOriginal(request);
+ }).bind(this);
+
+ this.active = true;
+ this.poller = new PeriodicalExecuter(function () {
+ var a = new Ajax.Request(url, ajaxOptions);
+ }.bindAsEventListener(this), interval || 3);
+ },
+ start: function () {
+ this.active = true;
+ this.container.removeClassName(this.options.classNames.inactive);
+ this.container.addClassName(this.options.classNames.active);
+ this.executer = new PeriodicalExecuter(this.step.bind(this, this.options.step), this.options.interval);
+ },
+ stop: function (reset) {
+ this.active = false;
+ if (this.executer) {
+ this.executer.stop();
+ }
+ this.container.removeClassName(this.options.classNames.active);
+ this.container.addClassName(this.options.classNames.inactive);
+ if (typeof reset === 'undefined' || reset === true) {
+ this.reset();
+ }
+ },
+ step: function (amount) {
+ this.active = true;
+ this.setProgress(Math.min(100, this.progress + amount));
+ },
+ reset: function () {
+ this.active = false;
+ this.setProgress(0);
+ },
+ draw: function () {
+ this.progressContainer.setStyle({
+ width: (100 - this.progress) + "%"
+ });
+ },
+ notify: function (event_name) {
+ if (this.options[event_name]) {
+ return [this.options[event_name].apply(this.options[event_name], $A(arguments).slice(1))];
+ }
+ }
+});
+Event.extend(Control.ProgressBar);
+}
diff --git a/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/sortable.js b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/sortable.js
new file mode 100644
index 0000000000..4caad61e86
--- /dev/null
+++ b/vendored-plugins/reporting_engine/vendor/assets/javascripts/reporting_engine/sortable.js
@@ -0,0 +1,311 @@
+/*
+Table sorting script by Joost de Valk, check it out at http://www.joostdevalk.nl/code/sortable-table/.
+Based on a script from http://www.kryogenix.org/code/browser/sorttable/.
+Distributed under the MIT license: http://www.kryogenix.org/code/browser/licence.html .
+
+Copyright (c) 1997-2007 Stuart Langridge, Joost de Valk.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Version 1.5.7
+*/
+
+/* You can change these values */
+var europeandate = true;
+var alternate_row_colors = true;
+
+/* Don't change anything below this unless you know what you're doing */
+// addEvent(window, "load", sortables_init);
+
+var SORT_COLUMN_INDEX;
+var thead = false;
+
+function alternate(table) {
+ // Take object table and get all it's tbodies.
+ var i, j, tableBodies, tableRows;
+ tableBodies = table.getElementsByTagName("tbody");
+ // Loop through these tbodies
+ for (i = 0; i < tableBodies.length; i += 1) {
+ // Take the tbody, and get all it's rows
+ tableRows = tableBodies[i].getElementsByTagName("tr");
+ // Loop through these rows
+ // Start at 1 because we want to leave the heading row untouched
+ for (j = 0; j < tableRows.length; j += 1) {
+ // Check if j is even, and apply classes for both possible results
+ if ((j % 2) === 0) {
+ if (tableRows[j].className.indexOf('odd') !== -1) {
+ tableRows[j].className = tableRows[j].className.replace('odd', 'even');
+ } else {
+ if (tableRows[j].className.indexOf('even') === -1) {
+ tableRows[j].className += " even";
+ }
+ }
+ } else {
+ if (tableRows[j].className.indexOf('even') !== -1) {
+ tableRows[j].className = tableRows[j].className.replace('even', 'odd');
+ } else {
+ if (tableRows[j].className.indexOf('odd') === -1) {
+ tableRows[j].className += " odd";
+ }
+ }
+ }
+ }
+ }
+}
+
+function ts_getInnerText(el) {
+ if (typeof el === "string") {
+ return el;
+ }
+ if (typeof el === "undefined") {
+ return el;
+ }
+ if (el.innerText) {
+ return el.innerText;
+ } //Not needed but it is faster
+ var str, cs, l, i;
+ str = "";
+
+ cs = el.childNodes;
+ l = cs.length;
+ for (i = 0; i < l; i += 1) {
+ switch (cs[i].nodeType) {
+ case 1: //ELEMENT_NODE
+ str += ts_getInnerText(cs[i]);
+ break;
+ case 3: //TEXT_NODE
+ str += cs[i].nodeValue;
+ break;
+ }
+ }
+ return str;
+}
+
+function ts_makeSortable(t) {
+ var firstRow, i, cell, txt;
+ if (t.rows && t.rows.length > 0) {
+ if (t.tHead && t.tHead.rows.length > 0) {
+ firstRow = t.tHead.rows[t.tHead.rows.length - 1];
+ thead = true;
+ } else {
+ firstRow = t.rows[0];
+ }
+ }
+ if (!firstRow) {
+ return;
+ }
+
+ // We have a first row: assume it's the header, and make its contents clickable links
+ for (i = 0; i < firstRow.cells.length; i += 1) {
+ cell = firstRow.cells[i];
+
+ var descendants = cell.descendants();
+
+ for(var j = 0; j < descendants.length; j++ ) {
+ if (descendants[j].descendants().length == 0) {
+ cell = descendants[j];
+ }
+ };
+
+ txt = cell.innerHTML;
+ if (cell.className !== "unsortable" && cell.className.indexOf("unsortable") === -1) {
+ cell.innerHTML = '';
+ }
+ }
+}
+
+function sortables_init() {
+ // Find table with id sortable-table and make them sortable
+ if (!document.getElementById) {
+ return;
+ }
+ var tbl = document.getElementById("sortable-table");
+ ts_makeSortable(tbl);
+}
+
+function getParent(el, pTagName) {
+ if (el === null) {
+ return null;
+ } else if (el.nodeType === 1 && el.tagName.toLowerCase() === pTagName.toLowerCase()) {
+ return el;
+ } else {
+ return getParent(el.parentNode, pTagName);
+ }
+}
+
+function ts_get_cell_data(a, idx) {
+ var acell, aa;
+ if ((typeof idx) === "undefined") {
+ acell = a.cells[SORT_COLUMN_INDEX];
+ } else {
+ acell = a.cells[idx];
+ }
+ if ((aa = acell.getAttribute("raw-data")) === null) {
+ aa = ts_getInnerText(acell).toLowerCase();
+ }
+ return aa;
+}
+
+function compare_numeric(a, b) {
+ var af, bf;
+ af = parseFloat(a);
+ af = (isNaN(af) ? 0 : af);
+ bf = parseFloat(b);
+ bf = (isNaN(bf) ? 0 : bf);
+ return af - bf;
+}
+
+function ts_sort_numeric(a, b) {
+ var cells = [ts_get_cell_data(a), ts_get_cell_data(b)];
+ return compare_numeric(cells[0], cells[1]);
+}
+
+function ts_sort_caseinsensitive(a, b) {
+ var cells = [ts_get_cell_data(a), ts_get_cell_data(b)];
+ if (cells[0] === cells[1]) {
+ return 0;
+ }
+ if (cells[0] < cells[1]) {
+ return -1;
+ }
+ return 1;
+}
+
+function trim(s) {
+ return s.replace(/^\s+|\s+$/g, "");
+}
+
+function ts_resortTable(lnk, clid) {
+ var td, column, t, first, itm, i, j, k, numeric_flag, all_sort_links, ci, firstRow, newRows, sortfn;
+ td = lnk.parentNode;
+ column = (clid === undefined ? td.cellIndex : clid);
+ t = getParent(td, 'TABLE');
+
+ // Do not sort single a row
+ if (t.rows.length <= 1) {
+ return;
+ }
+
+ // Determine if all rows are equal
+ first = ts_get_cell_data(t.tBodies[0].rows[0], 0);
+ itm = first;
+ i = 0;
+ while (itm === first && i < t.tBodies[0].rows.length) {
+ itm = ts_get_cell_data(t.tBodies[0].rows[i], column);
+ itm = trim(itm);
+ if (itm.substr(0, 4) === "