mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch 'dev' into fix/merge_wiki_content_into_page
This commit is contained in:
@@ -5,12 +5,12 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: "dev"
|
||||
open-pull-requests-limit: 1
|
||||
open-pull-requests-limit: 3
|
||||
versioning-strategy: lockfile-only
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: "dev"
|
||||
open-pull-requests-limit: 1
|
||||
open-pull-requests-limit: 3
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
@@ -28,6 +28,7 @@ jobs:
|
||||
echo "OP_ADMIN_USER_SEEDER_FORCE_PASSWORD_CHANGE=off" >> .env.pullpreview
|
||||
echo "OPENPROJECT_SHOW__SETTING__MISMATCH__WARNING=false" >> .env.pullpreview
|
||||
echo "OPENPROJECT_FEATURE__STORAGES__MODULE__ACTIVE=true" >> .env.pullpreview
|
||||
echo "OPENPROJECT_HSTS=false" >> .env.pullpreview
|
||||
- name: Boot as BIM edition
|
||||
if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/')
|
||||
run: |
|
||||
|
||||
@@ -30,6 +30,7 @@ source 'https://rubygems.org'
|
||||
|
||||
ruby '~> 3.2.1'
|
||||
|
||||
gem 'ox'
|
||||
gem 'actionpack-xml_parser', '~> 2.0.0'
|
||||
gem 'activemodel-serializers-xml', '~> 1.0.1'
|
||||
gem 'activerecord-import', '~> 1.4.0'
|
||||
@@ -78,7 +79,7 @@ gem 'htmldiff'
|
||||
gem 'stringex', '~> 2.8.5'
|
||||
|
||||
# CommonMark markdown parser with GFM extension
|
||||
gem 'commonmarker', '~> 0.23.7'
|
||||
gem 'commonmarker', '~> 0.23.9'
|
||||
|
||||
# HTML pipeline for transformations on text formatter output
|
||||
# such as sanitization or additional features
|
||||
@@ -105,7 +106,10 @@ gem 'email_validator', '~> 2.2.3'
|
||||
gem 'json_schemer', '~> 0.2.18'
|
||||
gem 'ruby-duration', '~> 3.2.0'
|
||||
|
||||
gem 'mail', '>= 2.8.1'
|
||||
# `config/initializers/mail_starttls_patch.rb` has also been patched to
|
||||
# fix STARTTLS handling until https://github.com/mikel/mail/pull/1536 is
|
||||
# released.
|
||||
gem 'mail', '= 2.8.1'
|
||||
|
||||
# provide compatible filesystem information for available storage
|
||||
gem 'sys-filesystem', '~> 1.4.0', require: false
|
||||
@@ -179,7 +183,7 @@ gem 'puma', '~> 6.1'
|
||||
gem 'puma-plugin-statsd', '~> 2.0'
|
||||
gem 'rack-timeout', '~> 0.6.3', require: "rack/timeout/base"
|
||||
|
||||
gem 'nokogiri', '~> 1.14.0'
|
||||
gem 'nokogiri', '~> 1.14.3'
|
||||
|
||||
gem 'carrierwave', '~> 1.3.1'
|
||||
gem 'carrierwave_direct', '~> 2.1.0'
|
||||
@@ -189,7 +193,7 @@ gem 'aws-sdk-core', '~> 3.107'
|
||||
# File upload via fog + screenshots on travis
|
||||
gem 'aws-sdk-s3', '~> 1.91'
|
||||
|
||||
gem 'openproject-token', '~> 2.2.0'
|
||||
gem 'openproject-token', '~> 3.0.1'
|
||||
|
||||
gem 'plaintext', '~> 0.3.2'
|
||||
|
||||
@@ -253,7 +257,7 @@ group :test do
|
||||
end
|
||||
|
||||
group :ldap do
|
||||
gem 'net-ldap', '~> 0.17.0'
|
||||
gem 'net-ldap', '~> 0.18.0'
|
||||
end
|
||||
|
||||
group :development do
|
||||
@@ -322,7 +326,7 @@ gem 'disposable', '~> 0.6.2'
|
||||
|
||||
platforms :mri, :mingw, :x64_mingw do
|
||||
group :postgres do
|
||||
gem 'pg', '~> 1.4.0'
|
||||
gem 'pg', '~> 1.5.0'
|
||||
end
|
||||
|
||||
# Support application loading when no database exists yet.
|
||||
|
||||
+44
-43
@@ -274,7 +274,7 @@ GEM
|
||||
activerecord (>= 4.2)
|
||||
acts_as_tree (2.9.1)
|
||||
activerecord (>= 3.0.0)
|
||||
addressable (2.8.2)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
afm (0.2.2)
|
||||
@@ -282,7 +282,7 @@ GEM
|
||||
airbrake-ruby (~> 6.0)
|
||||
airbrake-ruby (6.2.1)
|
||||
rbtree3 (~> 0.6)
|
||||
appsignal (3.4.0)
|
||||
appsignal (3.4.1)
|
||||
rack
|
||||
ast (2.4.2)
|
||||
attr_required (1.0.1)
|
||||
@@ -291,7 +291,7 @@ GEM
|
||||
awesome_nested_set (3.5.0)
|
||||
activerecord (>= 4.0.0, < 7.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.740.0)
|
||||
aws-partitions (1.754.0)
|
||||
aws-sdk-core (3.171.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
@@ -300,7 +300,7 @@ GEM
|
||||
aws-sdk-kms (1.63.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.120.0)
|
||||
aws-sdk-s3 (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
@@ -353,7 +353,7 @@ GEM
|
||||
with_advisory_lock (>= 4.0.0)
|
||||
coderay (1.1.3)
|
||||
colored2 (3.1.2)
|
||||
commonmarker (0.23.8)
|
||||
commonmarker (0.23.9)
|
||||
compare-xml (0.66)
|
||||
nokogiri (~> 1.8)
|
||||
concurrent-ruby (1.2.2)
|
||||
@@ -488,9 +488,9 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-gmail_v1 (0.25.0)
|
||||
google-apis-gmail_v1 (0.26.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
googleauth (1.5.0)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -521,7 +521,7 @@ GEM
|
||||
domain_name (~> 0.5)
|
||||
http_parser.rb (0.6.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
i18n (1.13.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
@@ -530,7 +530,7 @@ GEM
|
||||
ice_cube (0.16.4)
|
||||
interception (0.5)
|
||||
io-console (0.6.0)
|
||||
irb (1.6.3)
|
||||
irb (1.6.4)
|
||||
reline (>= 0.3.0)
|
||||
iso8601 (0.13.0)
|
||||
jmespath (1.6.2)
|
||||
@@ -554,7 +554,7 @@ GEM
|
||||
open4 (~> 1.0)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
lefthook (1.3.9)
|
||||
lefthook (1.3.10)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
listen (3.8.0)
|
||||
@@ -607,7 +607,7 @@ GEM
|
||||
net-imap (0.3.4)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.17.1)
|
||||
net-ldap (0.18.0)
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
@@ -616,10 +616,10 @@ GEM
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.14.2)
|
||||
nokogiri (1.14.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
oj (3.14.2)
|
||||
oj (3.14.3)
|
||||
okcomputer (1.18.4)
|
||||
omniauth-saml (1.10.3)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
@@ -638,16 +638,17 @@ GEM
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
openproject-token (2.2.0)
|
||||
openproject-token (3.0.1)
|
||||
activemodel
|
||||
os (1.1.4)
|
||||
ox (2.14.16)
|
||||
paper_trail (12.3.0)
|
||||
activerecord (>= 5.2)
|
||||
request_store (~> 1.1)
|
||||
parallel (1.22.1)
|
||||
parallel (1.23.0)
|
||||
parallel_tests (4.2.0)
|
||||
parallel
|
||||
parser (3.2.2.0)
|
||||
parser (3.2.2.1)
|
||||
ast (~> 2.4.1)
|
||||
pdf-core (0.9.0)
|
||||
pdf-inspector (1.3.0)
|
||||
@@ -658,7 +659,7 @@ GEM
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.4.6)
|
||||
pg (1.5.2)
|
||||
plaintext (0.3.4)
|
||||
activesupport (> 2.2.1)
|
||||
nokogiri (~> 1.10, >= 1.10.4)
|
||||
@@ -695,20 +696,20 @@ GEM
|
||||
eventmachine_httpserver
|
||||
http_parser.rb (~> 0.6.0)
|
||||
multi_json
|
||||
puma (6.2.1)
|
||||
puma (6.2.2)
|
||||
nio4r (~> 2.0)
|
||||
puma-plugin-statsd (2.4.0)
|
||||
puma (>= 5.0, < 7)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.2)
|
||||
rack (2.2.6.4)
|
||||
rack (2.2.7)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.0.0)
|
||||
rack-mini-profiler (3.1.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (2.2.0)
|
||||
activesupport
|
||||
@@ -717,7 +718,7 @@ GEM
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (3.0.5)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
@@ -763,13 +764,12 @@ GEM
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rbtree3 (0.7.0)
|
||||
rbtree3 (0.7.1)
|
||||
rdoc (6.5.0)
|
||||
psych (>= 4.0.0)
|
||||
recaptcha (5.12.3)
|
||||
json
|
||||
recaptcha (5.14.0)
|
||||
redcarpet (3.6.0)
|
||||
regexp_parser (2.7.0)
|
||||
regexp_parser (2.8.0)
|
||||
reline (0.3.3)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
@@ -797,9 +797,9 @@ GEM
|
||||
rspec-core (~> 3.12.0)
|
||||
rspec-expectations (~> 3.12.0)
|
||||
rspec-mocks (~> 3.12.0)
|
||||
rspec-core (3.12.1)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.2)
|
||||
rspec-expectations (3.12.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.5)
|
||||
@@ -816,7 +816,7 @@ GEM
|
||||
rspec-retry (0.6.2)
|
||||
rspec-core (> 3.3)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.49.0)
|
||||
rubocop (1.50.2)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.0.0)
|
||||
@@ -828,13 +828,13 @@ GEM
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.28.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.17.1)
|
||||
rubocop-capybara (2.18.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-rails (2.18.0)
|
||||
rubocop-rails (2.19.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.19.0)
|
||||
rubocop-rspec (2.20.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
ruby-duration (3.2.3)
|
||||
@@ -842,7 +842,7 @@ GEM
|
||||
i18n
|
||||
iso8601
|
||||
ruby-ole (1.2.12.2)
|
||||
ruby-prof (1.6.1)
|
||||
ruby-prof (1.6.3)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-saml (1.15.0)
|
||||
@@ -864,7 +864,7 @@ GEM
|
||||
sprockets-rails
|
||||
tilt
|
||||
secure_headers (6.5.0)
|
||||
selenium-webdriver (4.8.6)
|
||||
selenium-webdriver (4.9.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
@@ -890,9 +890,9 @@ GEM
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
ssrf_filter (1.1.1)
|
||||
stackprof (0.2.24)
|
||||
stringex (2.8.5)
|
||||
stringio (3.0.5)
|
||||
stackprof (0.2.25)
|
||||
stringex (2.8.6)
|
||||
stringio (3.0.6)
|
||||
structured_warnings (0.4.0)
|
||||
svg-graph (2.2.1)
|
||||
swd (2.0.2)
|
||||
@@ -929,7 +929,7 @@ GEM
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
view_component (2.82.0)
|
||||
view_component (3.0.0)
|
||||
activesupport (>= 5.2.0, < 8.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
method_source (~> 1.0)
|
||||
@@ -992,7 +992,7 @@ DEPENDENCIES
|
||||
cells-rails (~> 0.1.4)
|
||||
closure_tree (~> 7.4.0)
|
||||
colored2
|
||||
commonmarker (~> 0.23.7)
|
||||
commonmarker (~> 0.23.9)
|
||||
compare-xml (~> 0.66)
|
||||
costs!
|
||||
daemons
|
||||
@@ -1034,14 +1034,14 @@ DEPENDENCIES
|
||||
listen (~> 3.8.0)
|
||||
livingstyleguide (~> 2.1.0)
|
||||
lograge (~> 0.12.0)
|
||||
mail (>= 2.8.1)
|
||||
mail (= 2.8.1)
|
||||
matrix (~> 0.4.2)
|
||||
meta-tags (~> 2.18.0)
|
||||
mini_magick (~> 4.12.0)
|
||||
multi_json (~> 1.15.0)
|
||||
my_page!
|
||||
net-ldap (~> 0.17.0)
|
||||
nokogiri (~> 1.14.0)
|
||||
net-ldap (~> 0.18.0)
|
||||
nokogiri (~> 1.14.3)
|
||||
oj (~> 3.14.0)
|
||||
okcomputer (~> 1.18.1)
|
||||
omniauth!
|
||||
@@ -1066,14 +1066,15 @@ DEPENDENCIES
|
||||
openproject-reporting!
|
||||
openproject-storages!
|
||||
openproject-team_planner!
|
||||
openproject-token (~> 2.2.0)
|
||||
openproject-token (~> 3.0.1)
|
||||
openproject-two_factor_authentication!
|
||||
openproject-webhooks!
|
||||
openproject-xls_export!
|
||||
overviews!
|
||||
ox
|
||||
paper_trail (~> 12.3)
|
||||
parallel_tests (~> 4.0)
|
||||
pg (~> 1.4.0)
|
||||
pg (~> 1.5.0)
|
||||
plaintext (~> 0.3.2)
|
||||
posix-spawn (~> 0.3.13)
|
||||
prawn (~> 2.2)
|
||||
|
||||
@@ -60,9 +60,9 @@ module Projects
|
||||
# prevent adding another error if there is already one present
|
||||
return if errors.present?
|
||||
|
||||
subprojects = model.descendants
|
||||
return if subprojects.empty?
|
||||
return if user.allowed_to?(:archive_project, subprojects)
|
||||
active_subprojects = model.active_subprojects
|
||||
return if active_subprojects.empty?
|
||||
return if user.allowed_to?(:archive_project, active_subprojects)
|
||||
|
||||
errors.add :base, :archive_permission_missing_on_subprojects
|
||||
end
|
||||
|
||||
@@ -47,6 +47,7 @@ module Queries
|
||||
|
||||
attribute :column_names # => columns
|
||||
attribute :filters
|
||||
attribute :timestamps
|
||||
|
||||
attribute :sort_criteria # => sortBy
|
||||
attribute :group_by # => groupBy
|
||||
@@ -59,6 +60,7 @@ module Queries
|
||||
|
||||
validate :validate_project
|
||||
validate :user_allowed_to_make_public
|
||||
validate :timestamps_are_parsable
|
||||
|
||||
def validate_project
|
||||
errors.add :project, :error_not_found if project_id.present? && !project_visible?
|
||||
@@ -81,5 +83,13 @@ module Queries
|
||||
errors.add :public, :error_unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def timestamps_are_parsable
|
||||
invalid_timestamps = model.timestamps.reject(&:valid?)
|
||||
|
||||
if invalid_timestamps.any?
|
||||
errors.add :timestamps, :invalid, values: invalid_timestamps.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,8 @@ class AccountController < ApplicationController
|
||||
before_action :apply_csp_appends, only: %i[login]
|
||||
before_action :disable_api
|
||||
before_action :check_auth_source_sso_failure, only: :auth_source_sso_failed
|
||||
before_action :check_internal_login_enabled, only: :internal_login
|
||||
after_action :remove_internal_login_flag, only: :login
|
||||
|
||||
layout 'no_menu'
|
||||
|
||||
@@ -49,13 +51,18 @@ class AccountController < ApplicationController
|
||||
|
||||
if user.logged?
|
||||
redirect_after_login(user)
|
||||
elsif omniauth_direct_login?
|
||||
elsif omniauth_direct_login? && !session[:internal_login]
|
||||
direct_login(user)
|
||||
elsif request.post?
|
||||
authenticate_user
|
||||
end
|
||||
end
|
||||
|
||||
def internal_login
|
||||
session[:internal_login] = true
|
||||
redirect_to action: :login
|
||||
end
|
||||
|
||||
# Log out current user and redirect to welcome page
|
||||
def logout
|
||||
# Keep attributes from the session
|
||||
@@ -527,4 +534,12 @@ class AccountController < ApplicationController
|
||||
|
||||
append_content_security_policy_directives(appends)
|
||||
end
|
||||
|
||||
def check_internal_login_enabled
|
||||
render_404 unless omniauth_direct_login?
|
||||
end
|
||||
|
||||
def remove_internal_login_flag
|
||||
session.delete :internal_login
|
||||
end
|
||||
end
|
||||
|
||||
@@ -128,13 +128,7 @@ class CustomFieldsController < ApplicationController
|
||||
end
|
||||
|
||||
def get_custom_field_params
|
||||
custom_field_params = permitted_params.custom_field
|
||||
|
||||
if !EnterpriseToken.allows_to?(:multiselect_custom_fields)
|
||||
custom_field_params.delete :multi_value
|
||||
end
|
||||
|
||||
custom_field_params
|
||||
permitted_params.custom_field
|
||||
end
|
||||
|
||||
def find_custom_option
|
||||
|
||||
@@ -157,9 +157,7 @@ class OAuthClientsController < ApplicationController
|
||||
end
|
||||
|
||||
def nextcloud?
|
||||
@oauth_client&.integration && \
|
||||
@oauth_client.integration.is_a?(::Storages::Storage) && \
|
||||
@oauth_client.integration.provider_type == 'nextcloud'
|
||||
@oauth_client&.integration&.provider_type == ::Storages::Storage::PROVIDER_TYPE_NEXTCLOUD
|
||||
end
|
||||
|
||||
def get_redirect_uri
|
||||
|
||||
@@ -492,19 +492,3 @@ class RepositoriesController < ApplicationController
|
||||
ent.dup.force_encoding('UTF-8') == ent.dup.force_encoding('BINARY')
|
||||
end
|
||||
end
|
||||
|
||||
class Date
|
||||
def months_ago(date = Date.today)
|
||||
((date.year - year) * 12) + (date.month - month)
|
||||
end
|
||||
|
||||
def weeks_ago(date = Date.today)
|
||||
((date.year - year) * 52) + (date.cweek - cweek)
|
||||
end
|
||||
end
|
||||
|
||||
class String
|
||||
def with_leading_slash
|
||||
starts_with?('/') ? self : "/#{self}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,9 +36,13 @@ module IconsHelper
|
||||
%(<i class="#{classnames}" #{title} aria-hidden="true"></i>).html_safe
|
||||
end
|
||||
|
||||
def spot_icon(icon_name, title: nil)
|
||||
classnames = "spot-icon spot-icon_#{icon_name}"
|
||||
content_tag(:span, title, class: classnames.to_s)
|
||||
def spot_icon(icon_name, title: nil, size: nil, classnames: nil)
|
||||
size_class = if size.nil?
|
||||
""
|
||||
else
|
||||
"spot-icon_#{size}"
|
||||
end
|
||||
content_tag(:span, title, class: "spot-icon #{size_class} spot-icon_#{icon_name} #{classnames}")
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -45,8 +45,9 @@ module MetaTagsHelper
|
||||
firstWeekOfYear: locale_first_week_of_year,
|
||||
firstDayOfWeek: locale_first_day_of_week,
|
||||
environment: Rails.env,
|
||||
edition: OpenProject::Configuration.edition
|
||||
}
|
||||
edition: OpenProject::Configuration.edition,
|
||||
'asset-host': OpenProject::Configuration.rails_asset_host.presence
|
||||
}.compact
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -42,6 +42,8 @@ class Day < ApplicationRecord
|
||||
|
||||
delegate :name, to: :week_day, allow_nil: true
|
||||
|
||||
scope :working, -> { where(working: true) }
|
||||
|
||||
def self.default_scope
|
||||
today = Time.zone.today
|
||||
from = today.at_beginning_of_month
|
||||
@@ -56,6 +58,8 @@ class Day < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.from_sql(from:, to:)
|
||||
from = from.to_date
|
||||
to = to.to_date
|
||||
<<~SQL.squish
|
||||
(SELECT
|
||||
to_char(dd, 'YYYYMMDD')::integer id,
|
||||
@@ -75,6 +79,11 @@ class Day < ApplicationRecord
|
||||
SQL
|
||||
end
|
||||
|
||||
def self.last_working
|
||||
# Look up only from 8 days ago, because the Setting.working_days must have at least 1 working weekday.
|
||||
from_range(from: 8.days.ago, to: Time.zone.yesterday).where(working: true).last
|
||||
end
|
||||
|
||||
def week_day
|
||||
WeekDay.new(day: day_of_week)
|
||||
end
|
||||
|
||||
@@ -97,7 +97,7 @@ class EnterpriseToken < ApplicationRecord
|
||||
def invalid_domain?
|
||||
return false unless token_object&.validate_domain?
|
||||
|
||||
token_object.domain != Setting.host_name
|
||||
!token_object.valid_domain?(Setting.host_name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
class Enumeration < ApplicationRecord
|
||||
default_scope { order("#{Enumeration.table_name}.position ASC") }
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :project, optional: true
|
||||
|
||||
acts_as_list scope: 'type = \'#{type}\''
|
||||
acts_as_tree order: 'position ASC'
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#++
|
||||
|
||||
class MenuItem < ApplicationRecord
|
||||
belongs_to :parent, class_name: 'MenuItem'
|
||||
belongs_to :parent, class_name: 'MenuItem', optional: true
|
||||
has_many :children, -> {
|
||||
order('id ASC')
|
||||
}, class_name: 'MenuItem', dependent: :destroy, foreign_key: :parent_id
|
||||
|
||||
@@ -57,7 +57,7 @@ class NotificationSetting < ApplicationRecord
|
||||
]
|
||||
end
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :project, optional: true
|
||||
belongs_to :user
|
||||
|
||||
include Scopes::Scoped
|
||||
|
||||
@@ -342,6 +342,11 @@ class Project < ApplicationRecord
|
||||
parents | descendants # Set union
|
||||
end
|
||||
|
||||
# Returns an array of active subprojects.
|
||||
def active_subprojects
|
||||
project.descendants.where(active: true)
|
||||
end
|
||||
|
||||
class << self
|
||||
# builds up a project hierarchy helper structure for use with #project_tree_from_hierarchy
|
||||
#
|
||||
|
||||
+8
-25
@@ -1,5 +1,4 @@
|
||||
#-- copyright
|
||||
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
@@ -25,33 +24,17 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class AttributeHelpTextSeeder < Seeder
|
||||
def initialize; end
|
||||
|
||||
def seed_data!
|
||||
print_status ' ↳ Creating attribute help texts' do
|
||||
seed_attribute_help_texts
|
||||
#++
|
||||
module Projects::Exports
|
||||
module Formatters
|
||||
class Description < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute)
|
||||
attribute.to_sym == :description
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seed_attribute_help_texts
|
||||
help_texts = demo_data_for('attribute_help_texts')
|
||||
if help_texts.present?
|
||||
help_texts.each do |help_text_attr|
|
||||
print_status '.'
|
||||
create_attribute_help_text help_text_attr
|
||||
end
|
||||
def format(project, **)
|
||||
Rails::Html::FullSanitizer.new.sanitize(project.description)
|
||||
end
|
||||
end
|
||||
|
||||
def create_attribute_help_text(help_text_attr)
|
||||
help_text_attr[:type] = AttributeHelpText::WorkPackage
|
||||
|
||||
attribute_help_text = AttributeHelpText.new help_text_attr
|
||||
attribute_help_text.save
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,7 +26,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class Projects::Status < ActiveRecord::Base
|
||||
class Projects::Status < ApplicationRecord
|
||||
belongs_to :project
|
||||
|
||||
self.table_name = 'project_statuses'
|
||||
|
||||
@@ -34,7 +34,7 @@ module Query::Timestamps
|
||||
|
||||
# Returns the timestamps the query should be evaluated at.
|
||||
#
|
||||
# In the database, the timestamps are stored as ISO8601 strings.
|
||||
# In the database, the timestamps are stored as strings.
|
||||
# This method returns the timestamps as array of `Timestamp` objects.
|
||||
#
|
||||
# Timestamps can be absolute (e.g. a certain date and time) or relative
|
||||
@@ -42,14 +42,16 @@ module Query::Timestamps
|
||||
# call `timestamp.to_time`.
|
||||
#
|
||||
def timestamps
|
||||
return [Timestamp.now] unless OpenProject::FeatureDecisions.show_changes_active?
|
||||
|
||||
timestamps = super.collect do |timestamp_string|
|
||||
Timestamp.new(timestamp_string)
|
||||
end
|
||||
timestamps.any? ? timestamps : [Timestamp.now]
|
||||
end
|
||||
|
||||
def timestamps=(array)
|
||||
super(array.collect { |element| element.respond_to?(:iso8601) ? element.iso8601 : element })
|
||||
def timestamps=(params)
|
||||
super(Array(params).collect(&:to_s))
|
||||
end
|
||||
|
||||
# Does this query perform a historic search?
|
||||
|
||||
+111
-81
@@ -27,68 +27,108 @@
|
||||
#++
|
||||
|
||||
class Timestamp
|
||||
delegate :hash, to: :to_s
|
||||
|
||||
class Exception < StandardError; end
|
||||
|
||||
class TimestampParser
|
||||
DATE_KEYWORD_REGEX =
|
||||
/^(?:oneDayAgo|lastWorkingDay|oneWeekAgo|oneMonthAgo)@(?:([0-1]?[0-9]|2[0-3]):[0-5]?[0-9])$/
|
||||
|
||||
def initialize(string)
|
||||
@original_string = string
|
||||
end
|
||||
|
||||
def parse!
|
||||
@timestamp_string = self.class.substitute_special_shortcut_values(@original_string)
|
||||
|
||||
case @timestamp_string
|
||||
when /[+-]?P/ # ISO8601 "Period"
|
||||
ActiveSupport::Duration.parse(@timestamp_string).iso8601
|
||||
when DATE_KEYWORD_REGEX # Built in date keywords
|
||||
@timestamp_string
|
||||
else
|
||||
Time.zone.iso8601(@timestamp_string).iso8601
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
raise e.class, "The string \"#{@original_string}\" cannot be parsed to a Timestamp."
|
||||
end
|
||||
|
||||
class << self
|
||||
def substitute_special_shortcut_values(string)
|
||||
# map now to PT0S
|
||||
return 'PT0S' if string == 'now'
|
||||
|
||||
# map 1y to P1Y, 1m to P1M, 1w to P1W, 1d to P1D
|
||||
# map -1y to P-1Y, -1m to P-1M, -1w to P-1W, -1d to P-1D
|
||||
# map -1y1d to P-1Y-1D
|
||||
units = ['y', 'm', 'w', 'd']
|
||||
sign = '-' if string.start_with?('-')
|
||||
substitutions = units.map { |unit| string.scan(/\d+#{unit}/).first&.upcase }.compact
|
||||
|
||||
return string if substitutions.empty?
|
||||
|
||||
"P#{sign}#{substitutions.join(sign)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def parse(timestamp_string)
|
||||
return timestamp_string if timestamp_string.is_a?(Timestamp)
|
||||
|
||||
timestamp_string = TimestampParser.new(timestamp_string.strip).parse!
|
||||
new(timestamp_string)
|
||||
end
|
||||
|
||||
# Take a comma-separated string of ISO-8601 timestamps and convert it
|
||||
# into an array of Timestamp objects.
|
||||
#
|
||||
def parse_multiple(comma_separated_timestamp_string)
|
||||
comma_separated_timestamp_string.to_s.split(",").compact_blank.collect do |timestamp_string|
|
||||
Timestamp.parse(timestamp_string)
|
||||
end
|
||||
end
|
||||
|
||||
def now
|
||||
new(ActiveSupport::Duration.build(0).iso8601)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(arg = Timestamp.now.to_s)
|
||||
if arg.is_a? String
|
||||
@timestamp_iso8601_string = arg
|
||||
@timestamp_string = TimestampParser.substitute_special_shortcut_values(arg)
|
||||
elsif arg.respond_to? :iso8601
|
||||
@timestamp_iso8601_string = arg.iso8601
|
||||
@timestamp_string = arg.iso8601
|
||||
else
|
||||
raise Timestamp::Exception, \
|
||||
raise Timestamp::Exception,
|
||||
"Argument type not supported. " \
|
||||
"Please provide an ISO-8601 String or anything that responds to :iso8601, e.g. a Time."
|
||||
"Please provide an ISO-8601 or a relative date keyword String, or anything that responds to :iso8601, e.g. a Time."
|
||||
end
|
||||
end
|
||||
|
||||
def self.parse(iso8601_string)
|
||||
return iso8601_string if iso8601_string.is_a?(Timestamp)
|
||||
|
||||
iso8601_string = iso8601_string.strip
|
||||
iso8601_string = substitute_special_shortcut_values(iso8601_string)
|
||||
if iso8601_string.start_with? /[+-]?P/ # ISO8601 "Period"
|
||||
iso8601_string = ActiveSupport::Duration.parse(iso8601_string).iso8601
|
||||
elsif (time = Time.zone.parse(iso8601_string)).present?
|
||||
iso8601_string = time.iso8601
|
||||
else
|
||||
raise ArgumentError, "The string \"#{iso8601_string}\" cannot be parsed to Time or ActiveSupport::Duration."
|
||||
end
|
||||
Timestamp.new(iso8601_string)
|
||||
end
|
||||
|
||||
# Take a comma-separated string of ISO-8601 timestamps and convert it
|
||||
# into an array of Timestamp objects.
|
||||
#
|
||||
def self.parse_multiple(comma_separated_iso8601_string)
|
||||
comma_separated_iso8601_string.to_s.split(",").compact_blank.collect do |iso8601_string|
|
||||
Timestamp.parse(iso8601_string)
|
||||
end
|
||||
end
|
||||
|
||||
def self.now
|
||||
new(ActiveSupport::Duration.build(0).iso8601)
|
||||
end
|
||||
|
||||
def relative?
|
||||
duration? || relative_date_keyword?
|
||||
end
|
||||
|
||||
def duration?
|
||||
to_s.first == "P" # ISO8601 "Period"
|
||||
end
|
||||
|
||||
def relative_date_keyword?
|
||||
TimestampParser::DATE_KEYWORD_REGEX.match?(to_s)
|
||||
end
|
||||
|
||||
def to_s
|
||||
iso8601
|
||||
@timestamp_string.to_s
|
||||
end
|
||||
|
||||
def to_str
|
||||
to_s
|
||||
end
|
||||
|
||||
def iso8601
|
||||
@timestamp_iso8601_string.to_s
|
||||
end
|
||||
|
||||
def to_iso8601
|
||||
iso8601
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<Timestamp \"#{iso8601}\">"
|
||||
"#<Timestamp \"#{self}\">"
|
||||
end
|
||||
|
||||
def absolute
|
||||
@@ -96,21 +136,40 @@ class Timestamp
|
||||
end
|
||||
|
||||
def to_time
|
||||
if relative?
|
||||
Time.zone.now - (to_duration * (to_duration.to_i.positive? ? 1 : -1))
|
||||
if duration?
|
||||
Time.zone.now - to_duration.abs
|
||||
elsif relative_date_keyword?
|
||||
relative_date_keyword_to_time
|
||||
else
|
||||
Time.zone.parse(self)
|
||||
end
|
||||
end
|
||||
|
||||
def to_duration
|
||||
if relative?
|
||||
if duration?
|
||||
ActiveSupport::Duration.parse(self)
|
||||
else
|
||||
raise Timestamp::Exception, "This timestamp is absolute and cannot be represented as ActiveSupport::Duration."
|
||||
raise Timestamp::Exception, "This timestamp does not contain a duration cannot be represented as ActiveSupport::Duration."
|
||||
end
|
||||
end
|
||||
|
||||
def relative_date_keyword_to_time
|
||||
unless relative_date_keyword?
|
||||
raise ArgumentError, "This timestamp does not contain a relative date keyword and cannot be represented as Time."
|
||||
end
|
||||
|
||||
relative_date_keyword, time_part = @timestamp_string.split('@')
|
||||
|
||||
date = case relative_date_keyword
|
||||
when 'oneDayAgo' then 1.day.ago
|
||||
when 'lastWorkingDay' then Day.last_working.date || 1.day.ago
|
||||
when 'oneWeekAgo' then 1.week.ago
|
||||
when 'oneMonthAgo' then 1.month.ago
|
||||
end
|
||||
|
||||
Time.zone.parse(time_part, date)
|
||||
end
|
||||
|
||||
def as_json(*_args)
|
||||
to_s
|
||||
end
|
||||
@@ -122,9 +181,9 @@ class Timestamp
|
||||
def ==(other)
|
||||
case other
|
||||
when String
|
||||
iso8601 == other or to_s == other
|
||||
to_s == other
|
||||
when Timestamp
|
||||
iso8601 == other.iso8601
|
||||
to_s == other.to_s
|
||||
when NilClass
|
||||
to_s.blank?
|
||||
else
|
||||
@@ -140,38 +199,9 @@ class Timestamp
|
||||
self != Timestamp.now
|
||||
end
|
||||
|
||||
delegate :hash, to: :iso8601
|
||||
|
||||
class Exception < StandardError; end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def self.substitute_special_shortcut_values(string)
|
||||
# map now to PT0S
|
||||
string = "PT0S" if string == "now"
|
||||
|
||||
# map 1y to P1Y, 1m to P1M, 1w to P1W, 1d to P1D
|
||||
# map -1y to P-1Y, -1m to P-1M, -1w to P-1W, -1d to P-1D
|
||||
# map -1y1d to P-1Y-1D
|
||||
sign = "-" if string.start_with? "-"
|
||||
years = scan_for_shortcut_value(string:, unit: "y")
|
||||
months = scan_for_shortcut_value(string:, unit: "m")
|
||||
weeks = scan_for_shortcut_value(string:, unit: "w")
|
||||
days = scan_for_shortcut_value(string:, unit: "d")
|
||||
if years || months || weeks || days
|
||||
string = "P" \
|
||||
"#{sign if years}#{years}#{'Y' if years}" \
|
||||
"#{sign if months}#{months}#{'M' if months}" \
|
||||
"#{sign if weeks}#{weeks}#{'W' if weeks}" \
|
||||
"#{sign if days}#{days}#{'D' if days}"
|
||||
end
|
||||
|
||||
string
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
|
||||
def self.scan_for_shortcut_value(string:, unit:)
|
||||
string.scan(/(\d+)#{unit}/).flatten.first
|
||||
def valid?
|
||||
TimestampParser.new(to_s).parse!
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,6 +53,7 @@ class ::Type < ApplicationRecord
|
||||
association_foreign_key: 'custom_field_id'
|
||||
|
||||
belongs_to :color,
|
||||
optional: true,
|
||||
class_name: 'Color'
|
||||
|
||||
acts_as_list
|
||||
|
||||
+2
-1
@@ -54,7 +54,7 @@ class User < Principal
|
||||
inverse_of: :user
|
||||
has_one :rss_token, class_name: '::Token::RSS', dependent: :destroy
|
||||
has_one :api_token, class_name: '::Token::API', dependent: :destroy
|
||||
belongs_to :auth_source
|
||||
belongs_to :auth_source, optional: true
|
||||
|
||||
# Authorized OAuth grants
|
||||
has_many :oauth_grants,
|
||||
@@ -547,6 +547,7 @@ class User < Principal
|
||||
u.mail = ''
|
||||
u.status = User.statuses[:active]
|
||||
end).save
|
||||
|
||||
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
||||
end
|
||||
anonymous_user
|
||||
|
||||
+15
-10
@@ -49,11 +49,11 @@ class WorkPackage < ApplicationRecord
|
||||
belongs_to :type
|
||||
belongs_to :status, class_name: 'Status'
|
||||
belongs_to :author, class_name: 'User'
|
||||
belongs_to :assigned_to, class_name: 'Principal'
|
||||
belongs_to :responsible, class_name: 'Principal'
|
||||
belongs_to :version
|
||||
belongs_to :assigned_to, class_name: 'Principal', optional: true
|
||||
belongs_to :responsible, class_name: 'Principal', optional: true
|
||||
belongs_to :version, optional: true
|
||||
belongs_to :priority, class_name: 'IssuePriority'
|
||||
belongs_to :category, class_name: 'Category'
|
||||
belongs_to :category, class_name: 'Category', optional: true
|
||||
|
||||
has_many :time_entries, dependent: :delete_all
|
||||
|
||||
@@ -140,21 +140,26 @@ class WorkPackage < ApplicationRecord
|
||||
|
||||
acts_as_searchable columns: ['subject',
|
||||
"#{table_name}.description",
|
||||
"#{Journal.table_name}.notes"],
|
||||
{
|
||||
name: "#{Journal.table_name}.notes",
|
||||
scope: -> { Journal.for_work_package.where("journable_id = #{table_name}.id") }
|
||||
}],
|
||||
tsv_columns: [
|
||||
{
|
||||
table_name: Attachment.table_name,
|
||||
column_name: 'fulltext',
|
||||
normalization_type: :text
|
||||
normalization_type: :text,
|
||||
scope: -> { Attachment.where(container_type: name).where("container_id = #{table_name}.id") }
|
||||
},
|
||||
{
|
||||
table_name: Attachment.table_name,
|
||||
column_name: 'file',
|
||||
normalization_type: :filename
|
||||
normalization_type: :filename,
|
||||
scope: -> { Attachment.where(container_type: name).where("container_id = #{table_name}.id") }
|
||||
}
|
||||
],
|
||||
include: %i(project journals attachments),
|
||||
references: %i(projects journals attachments),
|
||||
include: %i(project journals),
|
||||
references: %i(projects),
|
||||
date_column: "#{quoted_table_name}.created_at",
|
||||
# sort by id so that limited eager loading doesn't break with postgresql
|
||||
order_column: "#{table_name}.id"
|
||||
@@ -580,7 +585,7 @@ class WorkPackage < ApplicationRecord
|
||||
private_class_method :count_and_group_by
|
||||
|
||||
def set_attachments_error_details
|
||||
if invalid_attachment = attachments.detect { |a| !a.valid? }
|
||||
if invalid_attachment = attachments.detect(&:invalid?)
|
||||
errors.messages[:attachments].first << " - #{invalid_attachment.errors.full_messages.first}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ module WorkPackages::Costs
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :budget, inverse_of: :work_packages
|
||||
belongs_to :budget, inverse_of: :work_packages, optional: true
|
||||
has_many :cost_entries, dependent: :delete_all
|
||||
|
||||
# disabled for now, implements part of ticket blocking
|
||||
|
||||
@@ -29,9 +29,9 @@ class AdminUserSeeder < Seeder
|
||||
def seed_data!
|
||||
user = new_admin
|
||||
unless user.save! validate: false
|
||||
puts 'Seeding admin failed:'
|
||||
print_error 'Seeding admin failed:'
|
||||
user.errors.full_messages.each do |msg|
|
||||
puts " #{msg}"
|
||||
print_error " #{msg}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ module BasicData
|
||||
def seed_data!
|
||||
data.each do |attributes|
|
||||
unless Role.find_by(builtin: attributes[:builtin]).nil?
|
||||
puts " *** Skipping built in role #{attributes[:name]} - already exists"
|
||||
print_status " *** Skipping built in role #{attributes[:name]} - already exists"
|
||||
next
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module BasicData
|
||||
class BuiltinUsersSeeder < Seeder
|
||||
def seed_data!
|
||||
User.system
|
||||
User.anonymous
|
||||
DeletedUser.first
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -58,7 +58,7 @@ module BasicData
|
||||
color_id: colors.fetch(color_name),
|
||||
is_in_roadmap:,
|
||||
is_milestone:,
|
||||
description: type_description(type_name)
|
||||
description: ''
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -71,39 +71,29 @@ module BasicData
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def type_description(type_name)
|
||||
return '' if demo_data_for('type_configuration').nil?
|
||||
|
||||
demo_data_for('type_configuration').each do |entry|
|
||||
if entry[:type] && I18n.t(entry[:type]) === I18n.t(type_name)
|
||||
return entry[:description] || ''
|
||||
else
|
||||
return ''
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_attribute_groups_for_type(type)
|
||||
return if demo_data_for('type_configuration').nil?
|
||||
type_data = type_data_for(type)
|
||||
return unless type_data && type_data['form_configuration']
|
||||
|
||||
demo_data_for('type_configuration').each do |entry|
|
||||
if entry[:form_configuration] && I18n.t(entry[:type]) === type.name
|
||||
type_data['form_configuration'].each do |form_config_attr|
|
||||
groups = type.default_attribute_groups
|
||||
query = find_query_by_name(form_config_attr['query_name'])
|
||||
query_association = "query_#{query}"
|
||||
groups.unshift([form_config_attr['group_name'], [query_association.to_sym]])
|
||||
|
||||
entry[:form_configuration].each do |form_config_attr|
|
||||
groups = type.default_attribute_groups
|
||||
query_association = 'query_' + find_query_by_name(form_config_attr[:query_name]).to_s
|
||||
groups.unshift([form_config_attr[:group_name], [query_association.to_sym]])
|
||||
|
||||
type.attribute_groups = groups
|
||||
end
|
||||
|
||||
type.save!
|
||||
end
|
||||
type.attribute_groups = groups
|
||||
end
|
||||
|
||||
type.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_data_for(type)
|
||||
types_data = seed_data.lookup('type_configuration') || []
|
||||
types_data.find { |entry| I18n.t(entry['type']) == type.name }
|
||||
end
|
||||
|
||||
def find_query_by_name(name)
|
||||
Query.find_by(name:).id
|
||||
end
|
||||
|
||||
@@ -28,65 +28,16 @@
|
||||
module BasicData
|
||||
class WorkflowSeeder < Seeder
|
||||
def seed_data!
|
||||
colors = Color.all
|
||||
colors = colors.map { |c| { c.name => c.id } }.reduce({}, :merge)
|
||||
fix_work_packages_without_types
|
||||
|
||||
if WorkPackage.where(type_id: nil).any? || Journal::WorkPackageJournal.where(type_id: nil).any?
|
||||
# Fixes work packages that do not have a type yet. They receive the standard type.
|
||||
#
|
||||
# This can happen when an existing database, having timelines planning elements,
|
||||
# gets migrated. During the migration, the existing planning elements are converted
|
||||
# to work_packages. Because the existence of a standard type cannot be guaranteed
|
||||
# during the migration, such work packages receive a type_id of nil.
|
||||
#
|
||||
# Because all work packages that do not have a type yet should always have had one
|
||||
# (from todays standpoint). The assignment is done covertedly.
|
||||
|
||||
WorkPackage.transaction do
|
||||
green_color = colors[I18n.t(:default_color_green_light)]
|
||||
standard_type = Type.find_or_create_by(is_standard: true,
|
||||
name: 'none',
|
||||
position: 0,
|
||||
color_id: green_color,
|
||||
is_default: true,
|
||||
is_in_roadmap: true,
|
||||
is_milestone: false)
|
||||
|
||||
[WorkPackage, Journal::WorkPackageJournal].each do |klass|
|
||||
klass.where(type_id: nil).update_all(type_id: standard_type.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Type.where(is_standard: false).any? || Status.any? || Workflow.any?
|
||||
puts ' *** Skipping types, statuses and workflows as there are already some configured'
|
||||
elsif Role.where(name: I18n.t(:default_role_member)).empty? ||
|
||||
Role.where(name: I18n.t(:default_role_project_admin)).empty?
|
||||
|
||||
puts ' *** Skipping types, statuses and workflows as the required roles do not exist'
|
||||
if any_types_or_statuses_or_workflows_already_configured?
|
||||
print_status ' *** Skipping types, statuses and workflows as there are already some configured'
|
||||
elsif required_roles_missing?
|
||||
print_status ' *** Skipping types, statuses and workflows as the required roles do not exist'
|
||||
else
|
||||
member = Role.where(name: I18n.t(:default_role_member)).first
|
||||
manager = Role.where(name: I18n.t(:default_role_project_admin)).first
|
||||
|
||||
puts ' ↳ Types'
|
||||
type_seeder_class.new.seed!
|
||||
|
||||
puts ' ↳ Statuses'
|
||||
status_seeder_class.new.seed!
|
||||
|
||||
# Workflow - Each type has its own workflow
|
||||
workflows.each do |type_id, statuses_for_type|
|
||||
statuses_for_type.each do |old_status|
|
||||
statuses_for_type.each do |new_status|
|
||||
[manager.id, member.id].each do |role_id|
|
||||
Workflow.create type_id:,
|
||||
role_id:,
|
||||
old_status_id: old_status.id,
|
||||
new_status_id: new_status.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
seed_statuses
|
||||
seed_types
|
||||
seed_workflows
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,5 +52,74 @@ module BasicData
|
||||
def status_seeder_class
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fix_work_packages_without_types
|
||||
if WorkPackage.where(type_id: nil).any? || Journal::WorkPackageJournal.where(type_id: nil).any?
|
||||
# Fixes work packages that do not have a type yet. They receive the standard type.
|
||||
#
|
||||
# This can happen when an existing database, having timelines planning elements,
|
||||
# gets migrated. During the migration, the existing planning elements are converted
|
||||
# to work_packages. Because the existence of a standard type cannot be guaranteed
|
||||
# during the migration, such work packages receive a type_id of nil.
|
||||
#
|
||||
# Because all work packages that do not have a type yet should always have had one
|
||||
# (from todays standpoint). The assignment is done covertly.
|
||||
|
||||
WorkPackage.transaction do
|
||||
green_color = Color.find_by(name: I18n.t(:default_color_green_light))
|
||||
standard_type = Type.find_or_create_by(is_standard: true,
|
||||
name: 'none',
|
||||
position: 0,
|
||||
color_id: green_color,
|
||||
is_default: true,
|
||||
is_in_roadmap: true,
|
||||
is_milestone: false)
|
||||
|
||||
[WorkPackage, Journal::WorkPackageJournal].each do |klass|
|
||||
klass.where(type_id: nil).update_all(type_id: standard_type.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def any_types_or_statuses_or_workflows_already_configured?
|
||||
Type.where(is_standard: false).any? || Status.any? || Workflow.any?
|
||||
end
|
||||
|
||||
def required_roles_missing?
|
||||
Role.where(name: I18n.t(:default_role_member)).empty? \
|
||||
|| Role.where(name: I18n.t(:default_role_project_admin)).empty?
|
||||
end
|
||||
|
||||
def seed_statuses
|
||||
print_status ' ↳ Statuses'
|
||||
status_seeder_class.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_types
|
||||
print_status ' ↳ Types'
|
||||
type_seeder_class.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_workflows
|
||||
member = Role.find_by(name: I18n.t(:default_role_member))
|
||||
manager = Role.find_by(name: I18n.t(:default_role_project_admin))
|
||||
|
||||
# Workflow - Each type has its own workflow
|
||||
workflows.each do |type_id, statuses_for_type|
|
||||
statuses_for_type.each do |old_status|
|
||||
statuses_for_type.each do |new_status|
|
||||
[manager, member].each do |role|
|
||||
Workflow.create type_id:,
|
||||
role:,
|
||||
old_status:,
|
||||
new_status:
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,23 +27,24 @@
|
||||
class CompositeSeeder < Seeder
|
||||
def seed_data!
|
||||
ActiveRecord::Base.transaction do
|
||||
data_seeders.each do |seeder|
|
||||
puts " ↳ #{seeder.class.name.demodulize}"
|
||||
seeder.seed!
|
||||
end
|
||||
seed_with(data_seeders)
|
||||
|
||||
return if discovered_seeders.empty?
|
||||
|
||||
puts " Loading discovered seeders: "
|
||||
discovered_seeders.each do |seeder|
|
||||
puts " ↳ #{seeder.class.name.demodulize}"
|
||||
seeder.seed!
|
||||
if discovered_seeders.any?
|
||||
print_status "Loading discovered seeders: #{discovered_seeders.map { seeder_name(_1) }.join(', ')}"
|
||||
seed_with(discovered_seeders)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def seed_with(seeders)
|
||||
seeders.each do |seeder|
|
||||
print_status " ↳ #{seeder_name(seeder)}"
|
||||
seeder.seed!
|
||||
end
|
||||
end
|
||||
|
||||
def data_seeders
|
||||
data_seeder_classes.map(&:new)
|
||||
instantiate(data_seeder_classes)
|
||||
end
|
||||
|
||||
def data_seeder_classes
|
||||
@@ -51,7 +52,7 @@ class CompositeSeeder < Seeder
|
||||
end
|
||||
|
||||
def discovered_seeders
|
||||
discovered_seeder_classes.map(&:new)
|
||||
instantiate(discovered_seeder_classes)
|
||||
end
|
||||
|
||||
##
|
||||
@@ -76,4 +77,12 @@ class CompositeSeeder < Seeder
|
||||
def include_discovered_class?(discovered_class)
|
||||
discovered_class.name =~ /^#{namespace}::/
|
||||
end
|
||||
|
||||
def seeder_name(seeder)
|
||||
seeder.class.name.split('::').without(namespace).join('::')
|
||||
end
|
||||
|
||||
def instantiate(seeder_classes)
|
||||
seeder_classes.map { |seeder_class| seeder_class.new(seed_data) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class GlobalQuerySeeder < Seeder
|
||||
def initialize; end
|
||||
|
||||
def seed_data!
|
||||
print_status ' ↳ Creating global queries' do
|
||||
seed_global_queries
|
||||
@@ -38,8 +36,8 @@ module DemoData
|
||||
private
|
||||
|
||||
def seed_global_queries
|
||||
Array(demo_data_for('global_queries')).each do |config|
|
||||
DemoData::QueryBuilder.new(config, nil).create!
|
||||
seed_data.each('global_queries') do |config|
|
||||
DemoData::QueryBuilder.new(config, project: nil, user:).create!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,14 +27,8 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class GroupSeeder < Seeder
|
||||
attr_accessor :user
|
||||
|
||||
include ::DemoData::References
|
||||
|
||||
def initialize
|
||||
self.user = User.admin.first
|
||||
end
|
||||
|
||||
def seed_data!
|
||||
print_status ' ↳ Creating groups' do
|
||||
seed_groups
|
||||
@@ -45,36 +39,11 @@ module DemoData
|
||||
Group.count.zero?
|
||||
end
|
||||
|
||||
def add_projects_to_groups
|
||||
groups = demo_data_for('groups')
|
||||
if groups.present?
|
||||
groups.each do |group_attr|
|
||||
if group_attr[:projects].present?
|
||||
group = Group.find_by(lastname: group_attr[:name])
|
||||
group_attr[:projects].each do |project_attr|
|
||||
project = Project.find(project_attr[:name])
|
||||
role = Role.find_by(name: project_attr[:role])
|
||||
|
||||
Member.create!(
|
||||
project:,
|
||||
principal: group,
|
||||
roles: [role]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seed_groups
|
||||
groups = demo_data_for('groups')
|
||||
if groups.present?
|
||||
groups.each do |group_attr|
|
||||
print_status '.'
|
||||
create_group group_attr[:name]
|
||||
end
|
||||
seed_data.each('groups') do |group_data|
|
||||
create_group group_data['name']
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
module DemoData
|
||||
class OverviewSeeder < Seeder
|
||||
include ::DemoData::References
|
||||
|
||||
def seed_data!
|
||||
print_status "*** Seeding Overview"
|
||||
|
||||
seed_data.each_data('projects') do |project_data|
|
||||
overview_data = overview_data(project_data)
|
||||
next unless overview_data
|
||||
|
||||
print_status " -Creating overview for #{project_data.lookup('name')}"
|
||||
|
||||
overview = create_overview(overview_data, project_data)
|
||||
|
||||
overview_data.each('widgets') do |widget_config|
|
||||
build_widget(overview, widget_config)
|
||||
end
|
||||
|
||||
overview.save!
|
||||
end
|
||||
|
||||
add_permission
|
||||
end
|
||||
|
||||
def applicable?
|
||||
Grids::Overview.count.zero? && demo_projects_exist?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def demo_projects_exist?
|
||||
identifiers = []
|
||||
seed_data.each_data('projects') do |project_data|
|
||||
identifiers << project_data.lookup('identifier')
|
||||
end
|
||||
|
||||
identifiers.all? { |identifier| Project.exists?(identifier:) }
|
||||
end
|
||||
|
||||
def build_widget(overview, widget_config)
|
||||
create_attachments!(overview, widget_config)
|
||||
|
||||
widget_options = widget_config['options']
|
||||
|
||||
text_with_references(overview, widget_options)
|
||||
query_id_references(overview, widget_options)
|
||||
|
||||
overview.widgets.build(widget_config.except('attachments'))
|
||||
end
|
||||
|
||||
def create_attachments!(overview, attributes)
|
||||
Array(attributes['attachments']).each do |file_name|
|
||||
attachment = overview.attachments.build
|
||||
attachment.author = user
|
||||
attachment.file = File.new(attachment_path(file_name))
|
||||
|
||||
attachment.save!
|
||||
end
|
||||
end
|
||||
|
||||
def attachment_path(file_name)
|
||||
Rails.root.join(
|
||||
"config/locales/media/#{I18n.locale}/#{file_name}"
|
||||
)
|
||||
end
|
||||
|
||||
def find_project(project_data)
|
||||
Project.find_by!(identifier: project_data.lookup('identifier'))
|
||||
end
|
||||
|
||||
def create_overview(overview_data, project_data)
|
||||
Grids::Overview.create(
|
||||
row_count: overview_data.lookup('row_count'),
|
||||
column_count: overview_data.lookup('column_count'),
|
||||
project: find_project(project_data)
|
||||
)
|
||||
end
|
||||
|
||||
def overview_data(project_data)
|
||||
project_data.lookup('project-overview')
|
||||
end
|
||||
|
||||
def text_with_references(overview, widget_options)
|
||||
if widget_options && widget_options['text']
|
||||
widget_options['text'] = with_references(widget_options['text'], overview.project)
|
||||
widget_options['text'] = link_attachments(widget_options['text'], overview.attachments)
|
||||
end
|
||||
end
|
||||
|
||||
def query_id_references(overview, widget_options)
|
||||
if widget_options && widget_options['queryId']
|
||||
widget_options['queryId'] = with_references(widget_options['queryId'], overview.project)
|
||||
end
|
||||
end
|
||||
|
||||
def add_permission
|
||||
Role
|
||||
.includes(:role_permissions)
|
||||
.where(role_permissions: { permission: 'edit_project' })
|
||||
.each do |role|
|
||||
role.add_permission!(:manage_overview)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,68 +30,36 @@ module DemoData
|
||||
# Careful: The seeding recreates the seeded project before it runs, so any changes
|
||||
# on the seeded project will be lost.
|
||||
def seed_data!
|
||||
puts ' ↳ Updating settings'
|
||||
print_status ' ↳ Updating settings'
|
||||
seed_settings
|
||||
|
||||
seed_projects = demo_data_for('projects').keys
|
||||
|
||||
seed_projects.each do |key|
|
||||
puts " ↳ Creating #{key} project..."
|
||||
|
||||
puts ' -Creating/Resetting project'
|
||||
project = reset_project key
|
||||
|
||||
puts ' -Setting project status.'
|
||||
set_project_status(project, key)
|
||||
|
||||
puts ' -Setting members.'
|
||||
set_members(project)
|
||||
|
||||
puts ' -Creating news.'
|
||||
seed_news(project, key)
|
||||
|
||||
puts ' -Assigning types.'
|
||||
set_types(project, key)
|
||||
|
||||
puts ' -Creating categories'
|
||||
seed_categories(project, key)
|
||||
|
||||
puts ' -Creating versions.'
|
||||
seed_versions(project, key)
|
||||
|
||||
puts ' -Creating queries.'
|
||||
seed_queries(project, key)
|
||||
|
||||
project_data_seeders(project, key).each do |seeder|
|
||||
puts " -#{seeder.class.name.demodulize}"
|
||||
seeder.seed!
|
||||
end
|
||||
|
||||
seed_data.each_data('projects') do |project_data|
|
||||
seed_project(project_data)
|
||||
Setting.demo_projects_available = true
|
||||
end
|
||||
|
||||
puts ' ↳ Assign groups to projects'
|
||||
set_groups
|
||||
|
||||
puts ' ↳ Update form configuration with global queries'
|
||||
print_status ' ↳ Update form configuration with global queries'
|
||||
set_form_configuration
|
||||
end
|
||||
|
||||
def seed_project(project_data)
|
||||
print_status " ↳ Creating project: #{project_data.lookup('name')}"
|
||||
|
||||
project = reset_project(project_data)
|
||||
set_project_status(project, project_data)
|
||||
set_members(project)
|
||||
seed_news(project, project_data)
|
||||
set_types(project, project_data)
|
||||
seed_categories(project, project_data)
|
||||
seed_versions(project, project_data)
|
||||
seed_queries(project, project_data)
|
||||
seed_project_content(project, project_data)
|
||||
end
|
||||
|
||||
def applicable?
|
||||
Project.count.zero?
|
||||
end
|
||||
|
||||
def project_data_seeders(project, key)
|
||||
seeders = [
|
||||
DemoData::WikiSeeder,
|
||||
DemoData::CustomFieldSeeder,
|
||||
DemoData::WorkPackageSeeder,
|
||||
DemoData::WorkPackageBoardSeeder
|
||||
]
|
||||
|
||||
seeders.map { |seeder| seeder.new project, key }
|
||||
end
|
||||
|
||||
def seed_settings
|
||||
seedable_welcome_settings
|
||||
.select { |k,| Settings::Definition[k].writable? }
|
||||
@@ -101,34 +69,37 @@ module DemoData
|
||||
end
|
||||
|
||||
def seedable_welcome_settings
|
||||
welcome = demo_data_for('welcome')
|
||||
welcome = seed_data.lookup('welcome')
|
||||
return {} if welcome.blank?
|
||||
|
||||
{
|
||||
welcome_title: welcome[:title],
|
||||
welcome_text: welcome[:text],
|
||||
welcome_title: welcome.lookup('title'),
|
||||
welcome_text: welcome.lookup('text'),
|
||||
welcome_on_homescreen: 1
|
||||
}
|
||||
end
|
||||
|
||||
def reset_project(key)
|
||||
delete_project(key)
|
||||
create_project(key)
|
||||
def reset_project(data)
|
||||
print_status ' -Creating/Resetting project'
|
||||
delete_project(data)
|
||||
create_project(data)
|
||||
end
|
||||
|
||||
def create_project(key)
|
||||
Project.create! project_data(key)
|
||||
def create_project(project_data)
|
||||
Project.create! project_data(project_data)
|
||||
end
|
||||
|
||||
def delete_project(key)
|
||||
if delete_me = find_project(key)
|
||||
def delete_project(data)
|
||||
if delete_me = find_project(data)
|
||||
delete_me.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def set_project_status(project, key)
|
||||
status_code = project_data_for(key, 'status.code')
|
||||
status_explanation = project_data_for(key, 'status.description')
|
||||
def set_project_status(project, project_data)
|
||||
print_status ' -Setting project status.'
|
||||
|
||||
status_code = project_data.lookup('status.code')
|
||||
status_explanation = project_data.lookup('status.description')
|
||||
|
||||
if status_code || status_explanation
|
||||
Projects::Status.create!(
|
||||
@@ -140,8 +111,9 @@ module DemoData
|
||||
end
|
||||
|
||||
def set_members(project)
|
||||
role = Role.find_by(name: translate_with_base_url(:default_role_project_admin))
|
||||
user = User.user.admin.first
|
||||
print_status ' -Setting members.'
|
||||
|
||||
role = Role.find_by(name: I18n.t(:default_role_project_admin))
|
||||
|
||||
Member.create!(
|
||||
project:,
|
||||
@@ -150,106 +122,99 @@ module DemoData
|
||||
)
|
||||
end
|
||||
|
||||
def set_groups
|
||||
DemoData::GroupSeeder.new.add_projects_to_groups
|
||||
end
|
||||
|
||||
def set_form_configuration
|
||||
Type.all.each do |type|
|
||||
BasicData::TypeSeeder.new.set_attribute_groups_for_type(type)
|
||||
BasicData::TypeSeeder.new(seed_data).set_attribute_groups_for_type(type)
|
||||
end
|
||||
end
|
||||
|
||||
def set_types(project, key)
|
||||
def set_types(project, project_data)
|
||||
print_status ' -Assigning types.'
|
||||
|
||||
project.types.clear
|
||||
Array(project_data_for(key, 'types')).each do |type_name|
|
||||
type = Type.find_by(name: translate_with_base_url(type_name))
|
||||
Array(project_data.lookup('types')).each do |type_name|
|
||||
type = Type.find_by(name: I18n.t(type_name))
|
||||
project.types << type
|
||||
end
|
||||
end
|
||||
|
||||
def seed_categories(project, key)
|
||||
Array(project_data_for(key, 'categories')).each do |cat_name|
|
||||
def seed_categories(project, project_data)
|
||||
print_status ' -Creating categories'
|
||||
|
||||
Array(project_data.lookup('categories')).each do |cat_name|
|
||||
project.categories.create name: cat_name
|
||||
end
|
||||
end
|
||||
|
||||
def seed_news(project, key)
|
||||
user = User.admin.first
|
||||
Array(project_data_for(key, 'news')).each do |news|
|
||||
News.create! project:, author: user, title: news[:title], summary: news[:summary], description: news[:description]
|
||||
def seed_news(project, project_data)
|
||||
print_status ' -Creating news.'
|
||||
|
||||
project_data.each('news') do |news|
|
||||
News.create!(project:,
|
||||
author: user,
|
||||
title: news['title'],
|
||||
summary: news['summary'],
|
||||
description: news['description'])
|
||||
end
|
||||
end
|
||||
|
||||
def seed_queries(project, key)
|
||||
Array(project_data_for(key, 'queries')).each do |config|
|
||||
QueryBuilder.new(config, project).create!
|
||||
def seed_queries(project, project_data)
|
||||
print_status ' -Creating queries.'
|
||||
|
||||
Array(project_data.lookup('queries')).each do |config|
|
||||
QueryBuilder.new(config, project:, user:).create!
|
||||
end
|
||||
end
|
||||
|
||||
def seed_versions(project, key)
|
||||
version_data = Array(project_data_for(key, 'versions'))
|
||||
def seed_versions(project, project_data)
|
||||
print_status ' -Creating versions.'
|
||||
|
||||
version_data.each do |attributes|
|
||||
VersionBuilder.new(attributes, project).create!
|
||||
project_data.each('versions') do |attributes|
|
||||
VersionBuilder.new(attributes, project:, user:).create!
|
||||
end
|
||||
end
|
||||
|
||||
def seed_board(project)
|
||||
Forum.create!(
|
||||
project:,
|
||||
name: demo_data_for('board.name'),
|
||||
description: demo_data_for('board.description')
|
||||
)
|
||||
def seed_project_content(project, project_data)
|
||||
project_content_seeder_classes.each do |seeder_class|
|
||||
print_status " -#{seeder_class.name.demodulize}"
|
||||
|
||||
seeder = seeder_class.new(project, project_data)
|
||||
seeder.seed!
|
||||
end
|
||||
end
|
||||
|
||||
# override to add additional seeders
|
||||
def project_content_seeder_classes
|
||||
[
|
||||
DemoData::WikiSeeder,
|
||||
DemoData::WorkPackageSeeder,
|
||||
DemoData::WorkPackageBoardSeeder
|
||||
]
|
||||
end
|
||||
|
||||
module Data
|
||||
module_function
|
||||
|
||||
def project_data(key)
|
||||
def project_data(project_data)
|
||||
{
|
||||
name: project_name(key),
|
||||
identifier: project_identifier(key),
|
||||
description: project_description(key),
|
||||
enabled_module_names: project_modules(key),
|
||||
types: project_types,
|
||||
parent_id: parent_project_id(key)
|
||||
name: project_data.lookup('name'),
|
||||
identifier: project_data.lookup('identifier'),
|
||||
description: project_data.lookup('description'),
|
||||
enabled_module_names: project_data.lookup('modules'),
|
||||
types: Type.all,
|
||||
parent: parent_project(project_data)
|
||||
}
|
||||
end
|
||||
|
||||
def parent_project_id(key)
|
||||
parent_project(key).try(:id)
|
||||
end
|
||||
|
||||
def parent_project(key)
|
||||
identifier = project_data_for(key, 'parent')
|
||||
return nil unless identifier.present?
|
||||
def parent_project(project_data)
|
||||
identifier = project_data.lookup('parent')
|
||||
return nil if identifier.blank?
|
||||
|
||||
Project.find_by(identifier:)
|
||||
end
|
||||
|
||||
def project_name(key)
|
||||
project_data_for(key, 'name')
|
||||
end
|
||||
|
||||
def project_identifier(key)
|
||||
project_data_for(key, 'identifier')
|
||||
end
|
||||
|
||||
def project_description(key)
|
||||
project_data_for(key, 'description')
|
||||
end
|
||||
|
||||
def project_types
|
||||
Type.all
|
||||
end
|
||||
|
||||
def project_modules(key)
|
||||
project_data_for(key, 'modules')
|
||||
end
|
||||
|
||||
def find_project(key)
|
||||
Project.find_by(identifier: project_identifier(key))
|
||||
def find_project(data)
|
||||
Project.find_by(identifier: data.lookup('identifier'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,11 +26,13 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class QueryBuilder < ::Seeder
|
||||
attr_reader :config, :project
|
||||
attr_reader :config, :project, :user
|
||||
|
||||
def initialize(config, project)
|
||||
@config = config
|
||||
def initialize(config, project:, user:)
|
||||
super()
|
||||
@config = config.with_indifferent_access
|
||||
@project = project
|
||||
@user = user
|
||||
end
|
||||
|
||||
def create!
|
||||
@@ -48,7 +50,7 @@ module DemoData
|
||||
def base_attributes
|
||||
{
|
||||
name: config[:name],
|
||||
user: User.admin.user.first,
|
||||
user:,
|
||||
public: config.fetch(:public, true),
|
||||
starred: config.fetch(:starred, false),
|
||||
show_hierarchies: config.fetch(:hierarchy, false),
|
||||
@@ -147,7 +149,7 @@ module DemoData
|
||||
|
||||
def set_type_filter!(filters)
|
||||
types = Type
|
||||
.where(name: Array(config[:type]).map { |name| translate_with_base_url(name) })
|
||||
.where(name: Array(config[:type]).map { |name| I18n.t(name) })
|
||||
.pluck(:id)
|
||||
|
||||
if types.any?
|
||||
|
||||
@@ -28,11 +28,12 @@ module DemoData
|
||||
class VersionBuilder
|
||||
include ::DemoData::References
|
||||
|
||||
attr_reader :config, :project
|
||||
attr_reader :config, :project, :user
|
||||
|
||||
def initialize(config, project)
|
||||
def initialize(config, project:, user:)
|
||||
@config = config
|
||||
@project = project
|
||||
@user = user
|
||||
end
|
||||
|
||||
def create!
|
||||
@@ -53,13 +54,13 @@ module DemoData
|
||||
|
||||
def version
|
||||
version = Version.create!(
|
||||
name: config[:name],
|
||||
status: config[:status],
|
||||
sharing: config[:sharing],
|
||||
name: config['name'],
|
||||
status: config['status'],
|
||||
sharing: config['sharing'],
|
||||
project:
|
||||
)
|
||||
|
||||
set_wiki! version, config[:wiki] if config[:wiki]
|
||||
set_wiki! version, config['wiki'] if config['wiki']
|
||||
|
||||
version
|
||||
end
|
||||
|
||||
@@ -26,19 +26,18 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class WikiSeeder < Seeder
|
||||
attr_reader :project, :key
|
||||
attr_reader :project, :project_data
|
||||
|
||||
def initialize(project, key)
|
||||
def initialize(project, project_data)
|
||||
super()
|
||||
@project = project
|
||||
@key = key
|
||||
@project_data = project_data
|
||||
end
|
||||
|
||||
def seed_data!
|
||||
text = project_data_for(key, 'wiki')
|
||||
text = project_data.lookup('wiki')
|
||||
|
||||
return if text.is_a?(String) && text.start_with?("translation missing")
|
||||
|
||||
user = User.admin.first
|
||||
return if text.blank?
|
||||
|
||||
if text.is_a? String
|
||||
text = [{ title: "Wiki", content: text }]
|
||||
@@ -53,12 +52,10 @@ module DemoData
|
||||
user:
|
||||
)
|
||||
end
|
||||
|
||||
puts
|
||||
end
|
||||
|
||||
def create_wiki_page!(data, project:, user:, parent: nil)
|
||||
wiki_page = WikiPage.create!(
|
||||
WikiPage.create!(
|
||||
wiki: project.wiki,
|
||||
title: data[:title],
|
||||
parent:,
|
||||
|
||||
@@ -27,78 +27,80 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class WorkPackageBoardSeeder < Seeder
|
||||
attr_accessor :project, :key
|
||||
attr_reader :project, :project_data
|
||||
|
||||
include ::DemoData::References
|
||||
|
||||
def initialize(project, key)
|
||||
self.project = project
|
||||
self.key = key
|
||||
def initialize(project, project_data)
|
||||
super()
|
||||
@project = project
|
||||
@project_data = project_data
|
||||
end
|
||||
|
||||
def seed_data!
|
||||
# Seed only for those projects that provide a `kanban` key, i.e. 'demo-project' in standard edition.
|
||||
if project_has_data_for?(key, 'boards.kanban')
|
||||
if board_data = project_data.lookup('boards.kanban')
|
||||
print_status ' ↳ Creating demo status board' do
|
||||
seed_kanban_board
|
||||
seed_kanban_board(board_data)
|
||||
end
|
||||
Setting.boards_demo_data_available = 'true'
|
||||
end
|
||||
|
||||
if project_has_data_for?(key, 'boards.basic')
|
||||
if board_data = project_data.lookup('boards.basic')
|
||||
print_status ' ↳ Creating demo basic board' do
|
||||
seed_basic_board
|
||||
seed_basic_board(board_data)
|
||||
end
|
||||
end
|
||||
|
||||
if project_has_data_for?(key, 'boards.parent_child')
|
||||
if board_data = project_data.lookup('boards.parent_child')
|
||||
print_status ' ↳ Creating demo parent child board' do
|
||||
seed_parent_child_board
|
||||
seed_parent_child_board(board_data)
|
||||
end
|
||||
Setting.boards_demo_data_available = 'true'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seed_kanban_board
|
||||
board = ::Boards::Grid.new(project:)
|
||||
def seed_kanban_board(board_data)
|
||||
widgets = seed_kanban_board_widgets
|
||||
board =
|
||||
::Boards::Grid.new(
|
||||
project:,
|
||||
name: board_data.lookup('name'),
|
||||
options: { 'type' => 'action', 'attribute' => 'status', 'highlightingMode' => 'priority' },
|
||||
widgets:,
|
||||
column_count: widgets.count,
|
||||
row_count: 1
|
||||
)
|
||||
set_board_filters(board, board_data)
|
||||
board.save!
|
||||
end
|
||||
|
||||
board.name = project_data_for(key, 'boards.kanban.name')
|
||||
board.options = { 'type' => 'action', 'attribute' => 'status', 'highlightingMode' => 'priority' }
|
||||
|
||||
set_board_filters(board)
|
||||
|
||||
board.widgets = seed_kanban_board_queries.each_with_index.map do |query, i|
|
||||
def seed_kanban_board_widgets
|
||||
seed_kanban_board_queries.each_with_index.map do |query, i|
|
||||
Grids::Widget.new start_row: 1, end_row: 2,
|
||||
start_column: i + 1, end_column: i + 2,
|
||||
options: { query_id: query.id,
|
||||
filters: [{ status: { operator: '=', values: query.filters[0].values } }] },
|
||||
identifier: 'work_package_query'
|
||||
end
|
||||
|
||||
board.column_count = board.widgets.count
|
||||
board.row_count = 1
|
||||
|
||||
board.save!
|
||||
|
||||
Setting.boards_demo_data_available = 'true'
|
||||
end
|
||||
|
||||
def set_board_filters(board)
|
||||
if project_data_for(key, 'boards.kanban.filters').present?
|
||||
filters_conf = project_data_for(key, 'boards.kanban.filters')
|
||||
board.options[:filters] = []
|
||||
filters_conf.each do |filter|
|
||||
if filter[:type]
|
||||
type = Type.find_by(name: translate_with_base_url(filter[:type]))
|
||||
board.options[:filters] << { type: { operator: '=', values: [type.id.to_s] } }
|
||||
end
|
||||
def set_board_filters(board, board_data)
|
||||
filters_conf = board_data.lookup('filters')
|
||||
return if filters_conf.blank?
|
||||
|
||||
board.options[:filters] = []
|
||||
filters_conf.each do |filter|
|
||||
if filter['type']
|
||||
type = Type.find_by(name: I18n.t(filter['type']))
|
||||
board.options[:filters] << { type: { operator: '=', values: [type.id.to_s] } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def seed_kanban_board_queries
|
||||
admin = User.admin.first
|
||||
|
||||
status_names = ['New', 'In progress', 'Closed', 'Rejected']
|
||||
statuses = Status.where(name: status_names).to_a
|
||||
|
||||
@@ -107,7 +109,7 @@ module DemoData
|
||||
end
|
||||
|
||||
statuses.to_a.map do |status|
|
||||
Query.new_default(project:, user: admin).tap do |query|
|
||||
Query.new_default(project:, user:).tap do |query|
|
||||
# Make it public so that new members can see it too
|
||||
query.public = true
|
||||
|
||||
@@ -123,28 +125,31 @@ module DemoData
|
||||
end
|
||||
end
|
||||
|
||||
def seed_basic_board
|
||||
board = ::Boards::Grid.new(project:)
|
||||
board.name = project_data_for(key, 'boards.basic.name')
|
||||
board.options = { 'highlightingMode' => 'priority' }
|
||||
def seed_basic_board(board_data)
|
||||
widgets = seed_basic_board_widgets
|
||||
board =
|
||||
::Boards::Grid.new(
|
||||
project:,
|
||||
name: board_data.lookup('name'),
|
||||
options: { 'highlightingMode' => 'priority' },
|
||||
widgets:,
|
||||
column_count: widgets.count,
|
||||
row_count: 1
|
||||
)
|
||||
board.save!
|
||||
end
|
||||
|
||||
board.widgets = seed_basic_board_queries.each_with_index.map do |query, i|
|
||||
def seed_basic_board_widgets
|
||||
seed_basic_board_queries.each_with_index.map do |query, i|
|
||||
Grids::Widget.new start_row: 1, end_row: 2,
|
||||
start_column: i + 1, end_column: i + 2,
|
||||
options: { query_id: query.id,
|
||||
filters: [{ manualSort: { operator: 'ow', values: [] } }] },
|
||||
identifier: 'work_package_query'
|
||||
end
|
||||
|
||||
board.column_count = board.widgets.count
|
||||
board.row_count = 1
|
||||
|
||||
board.save!
|
||||
end
|
||||
|
||||
def seed_basic_board_queries
|
||||
admin = User.admin.first
|
||||
|
||||
wps = if project.name === 'Scrum project'
|
||||
scrum_query_work_packages
|
||||
else
|
||||
@@ -157,7 +162,7 @@ module DemoData
|
||||
{ name: 'Never', wps: wps[3] }]
|
||||
|
||||
lists.map do |list|
|
||||
Query.new(project:, user: admin).tap do |query|
|
||||
Query.new(project:, user:).tap do |query|
|
||||
# Make it public so that new members can see it too
|
||||
query.public = true
|
||||
query.include_subprojects = true
|
||||
@@ -198,36 +203,36 @@ module DemoData
|
||||
]
|
||||
end
|
||||
|
||||
def seed_parent_child_board
|
||||
board = ::Boards::Grid.new(project:)
|
||||
def seed_parent_child_board(board_data)
|
||||
widgets = seed_parent_child_board_widgets
|
||||
board =
|
||||
::Boards::Grid.new(
|
||||
project:,
|
||||
name: board_data.lookup('name'),
|
||||
options: { 'type' => 'action', 'attribute' => 'subtasks' },
|
||||
widgets:,
|
||||
column_count: widgets.count,
|
||||
row_count: 1
|
||||
)
|
||||
board.save!
|
||||
end
|
||||
|
||||
board.name = project_data_for(key, 'boards.parent_child.name')
|
||||
board.options = { 'type' => 'action', 'attribute' => 'subtasks' }
|
||||
|
||||
board.widgets = seed_parent_child_board_queries.each_with_index.map do |query, i|
|
||||
def seed_parent_child_board_widgets
|
||||
seed_parent_child_board_queries.each_with_index.map do |query, i|
|
||||
Grids::Widget.new start_row: 1, end_row: 2,
|
||||
start_column: i + 1, end_column: i + 2,
|
||||
options: { query_id: query.id,
|
||||
filters: [{ parent: { operator: '=', values: query.filters[1].values } }] },
|
||||
identifier: 'work_package_query'
|
||||
end
|
||||
|
||||
board.column_count = board.widgets.count
|
||||
board.row_count = 1
|
||||
|
||||
board.save!
|
||||
|
||||
Setting.boards_demo_data_available = 'true'
|
||||
end
|
||||
|
||||
def seed_parent_child_board_queries
|
||||
admin = User.admin.first
|
||||
|
||||
parents = [WorkPackage.find_by(subject: 'Organize open source conference'),
|
||||
WorkPackage.find_by(subject: 'Follow-up tasks')]
|
||||
|
||||
parents.map do |parent|
|
||||
Query.new_default(project:, user: admin).tap do |query|
|
||||
Query.new_default(project:, user:).tap do |query|
|
||||
# Make it public so that new members can see it too
|
||||
query.public = true
|
||||
|
||||
|
||||
@@ -26,34 +26,31 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
module DemoData
|
||||
class WorkPackageSeeder < Seeder
|
||||
attr_accessor :project, :user, :statuses, :repository,
|
||||
:types, :key
|
||||
attr_reader :project, :statuses, :repository,
|
||||
:types, :project_data
|
||||
|
||||
include ::DemoData::References
|
||||
|
||||
def initialize(project, key)
|
||||
self.project = project
|
||||
self.key = key
|
||||
self.user = User.user.admin.first
|
||||
self.statuses = Status.all
|
||||
self.repository = Repository.first
|
||||
self.types = project.types.all.reject(&:is_milestone?)
|
||||
def initialize(project, project_data)
|
||||
super()
|
||||
@project = project
|
||||
@project_data = project_data
|
||||
@statuses = Status.all
|
||||
@repository = Repository.first
|
||||
@types = project.types.all.reject(&:is_milestone?)
|
||||
end
|
||||
|
||||
def seed_data!
|
||||
print_status ' ↳ Creating work_packages' do
|
||||
seed_demo_work_packages
|
||||
set_workpackage_relations
|
||||
set_work_package_relations
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seed_demo_work_packages
|
||||
work_packages_data = project_data_for(key, 'work_packages')
|
||||
|
||||
work_packages_data.each do |attributes|
|
||||
print_status '.'
|
||||
project_data.each('work_packages') do |attributes|
|
||||
create_or_update_work_package(attributes)
|
||||
end
|
||||
end
|
||||
@@ -89,8 +86,7 @@ module DemoData
|
||||
end
|
||||
|
||||
def create_children!(work_package, attributes)
|
||||
Array(attributes[:children]).each do |child_attributes|
|
||||
print_status '.'
|
||||
Array(attributes['children']).each do |child_attributes|
|
||||
child = create_work_package child_attributes
|
||||
|
||||
child.parent = work_package
|
||||
@@ -102,13 +98,13 @@ module DemoData
|
||||
{
|
||||
project:,
|
||||
author: user,
|
||||
assigned_to: find_principal(attributes[:assignee]),
|
||||
subject: attributes[:subject],
|
||||
description: attributes[:description],
|
||||
assigned_to: find_principal(attributes['assignee']),
|
||||
subject: attributes['subject'],
|
||||
description: attributes['description'],
|
||||
status: find_status(attributes),
|
||||
type: find_type(attributes),
|
||||
priority: find_priority(attributes) || IssuePriority.default,
|
||||
parent: WorkPackage.find_by(subject: attributes[:parent])
|
||||
parent: WorkPackage.find_by(subject: attributes['parent'])
|
||||
}
|
||||
end
|
||||
|
||||
@@ -122,26 +118,26 @@ module DemoData
|
||||
end
|
||||
|
||||
def find_priority(attributes)
|
||||
IssuePriority.find_by(name: translate_with_base_url(attributes[:priority]))
|
||||
IssuePriority.find_by(name: I18n.t(attributes['priority']))
|
||||
end
|
||||
|
||||
def find_status(attributes)
|
||||
Status.find_by!(name: translate_with_base_url(attributes[:status]))
|
||||
Status.find_by!(name: I18n.t(attributes['status']))
|
||||
end
|
||||
|
||||
def find_type(attributes)
|
||||
Type.find_by!(name: translate_with_base_url(attributes[:type]))
|
||||
Type.find_by!(name: I18n.t(attributes['type']))
|
||||
end
|
||||
|
||||
def set_version!(wp_attr, attributes)
|
||||
if attributes[:version]
|
||||
wp_attr[:version] = Version.find_by!(name: attributes[:version])
|
||||
if attributes['version']
|
||||
wp_attr[:version] = Version.find_by!(name: attributes['version'])
|
||||
end
|
||||
end
|
||||
|
||||
def set_accountable!(wp_attr, attributes)
|
||||
if attributes[:accountable]
|
||||
wp_attr[:responsible] = find_principal(attributes[:accountable])
|
||||
if attributes['accountable']
|
||||
wp_attr[:responsible] = find_principal(attributes['accountable'])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -155,13 +151,13 @@ module DemoData
|
||||
|
||||
def set_backlogs_attributes!(wp_attr, attributes)
|
||||
if defined? OpenProject::Backlogs
|
||||
wp_attr[:position] = attributes[:position].to_i if attributes[:position].present?
|
||||
wp_attr[:story_points] = attributes[:story_points].to_i if attributes[:story_points].present?
|
||||
wp_attr[:position] = attributes['position'].to_i if attributes['position'].present?
|
||||
wp_attr[:story_points] = attributes['story_points'].to_i if attributes['story_points'].present?
|
||||
end
|
||||
end
|
||||
|
||||
def create_attachments!(work_package, attributes)
|
||||
Array(attributes[:attachments]).each do |file_name|
|
||||
Array(attributes['attachments']).each do |file_name|
|
||||
attachment = work_package.attachments.build
|
||||
attachment.author = work_package.author
|
||||
attachment.file = File.new("config/locales/media/en/#{file_name}")
|
||||
@@ -170,27 +166,25 @@ module DemoData
|
||||
end
|
||||
end
|
||||
|
||||
def set_workpackage_relations
|
||||
work_packages_data = project_data_for(key, 'work_packages')
|
||||
|
||||
work_packages_data.each do |attributes|
|
||||
def set_work_package_relations
|
||||
project_data.each('work_packages') do |attributes|
|
||||
create_relations attributes
|
||||
end
|
||||
end
|
||||
|
||||
def create_relations(attributes)
|
||||
Array(attributes[:relations]).each do |relation|
|
||||
root_work_package = WorkPackage.find_by!(subject: attributes[:subject])
|
||||
to_work_package = WorkPackage.find_by(subject: relation[:to], project: root_work_package.project)
|
||||
to_work_package = WorkPackage.find_by!(subject: relation[:to]) unless to_work_package.nil?
|
||||
Array(attributes['relations']).each do |relation|
|
||||
root_work_package = WorkPackage.find_by!(subject: attributes['subject'])
|
||||
to_work_package = WorkPackage.find_by(subject: relation['to'], project: root_work_package.project)
|
||||
to_work_package ||= WorkPackage.find_by!(subject: relation['to'])
|
||||
create_relation(
|
||||
to: to_work_package,
|
||||
from: root_work_package,
|
||||
type: relation[:type]
|
||||
type: relation['type']
|
||||
)
|
||||
end
|
||||
|
||||
Array(attributes[:children]).each do |child_attributes|
|
||||
Array(attributes['children']).each do |child_attributes|
|
||||
create_relations child_attributes
|
||||
end
|
||||
end
|
||||
@@ -223,12 +217,12 @@ module DemoData
|
||||
end
|
||||
|
||||
def start_date
|
||||
days_ahead = attributes[:start] || 0
|
||||
days_ahead = attributes['start'] || 0
|
||||
Time.zone.today.monday + days_ahead.days
|
||||
end
|
||||
|
||||
def due_date
|
||||
all_days.due_date(start_date, attributes[:duration])
|
||||
all_days.due_date(start_date, attributes['duration'])
|
||||
end
|
||||
|
||||
def duration
|
||||
@@ -242,7 +236,7 @@ module DemoData
|
||||
end
|
||||
|
||||
def estimated_hours
|
||||
attributes[:estimated_hours]&.to_i
|
||||
attributes['estimated_hours']&.to_i
|
||||
end
|
||||
|
||||
def all_days
|
||||
|
||||
@@ -28,7 +28,6 @@ class DemoDataSeeder < CompositeSeeder
|
||||
def data_seeder_classes
|
||||
[
|
||||
DemoData::GroupSeeder,
|
||||
DemoData::AttributeHelpTextSeeder,
|
||||
DemoData::GlobalQuerySeeder,
|
||||
DemoData::ProjectSeeder,
|
||||
DemoData::OverviewSeeder
|
||||
|
||||
@@ -31,11 +31,9 @@ module DevelopmentData
|
||||
print_status ' ↳ Creating custom fields...'
|
||||
cfs = create_cfs!
|
||||
|
||||
print_status "\n ↳ Creating types for linking CFs"
|
||||
print_status ' ↳ Creating types for linking CFs'
|
||||
create_types!(cfs)
|
||||
end
|
||||
|
||||
puts
|
||||
end
|
||||
|
||||
def all_cfs
|
||||
@@ -48,14 +46,12 @@ module DevelopmentData
|
||||
type = FactoryBot.build :type, name: 'All CFS'
|
||||
extend_group(type, ['Custom fields', non_req_cfs])
|
||||
type.save!
|
||||
print_status '.'
|
||||
|
||||
# Create type
|
||||
req_cfs = cfs.select(&:is_required).map(&:attribute_name)
|
||||
type_req = FactoryBot.build :type, name: 'Required CF'
|
||||
extend_group(type_req, ['Custom fields', req_cfs])
|
||||
type_req.save!
|
||||
print_status '.'
|
||||
end
|
||||
|
||||
def create_cfs!
|
||||
@@ -67,7 +63,6 @@ module DevelopmentData
|
||||
type: 'WorkPackageCustomField',
|
||||
is_required: false,
|
||||
field_format: type)
|
||||
print_status '.'
|
||||
end
|
||||
|
||||
cfs << CustomField.create!(name: "CF DEV list",
|
||||
@@ -75,7 +70,6 @@ module DevelopmentData
|
||||
type: 'WorkPackageCustomField',
|
||||
possible_values: ['A', 'B', 'C'],
|
||||
field_format: 'list')
|
||||
print_status '.'
|
||||
|
||||
cfs << CustomField.create!(name: "CF DEV multilist",
|
||||
type: 'WorkPackageCustomField',
|
||||
@@ -83,20 +77,17 @@ module DevelopmentData
|
||||
multi_value: true,
|
||||
possible_values: ['Foo', 'Bar', 'Bla'],
|
||||
field_format: 'list')
|
||||
print_status '.'
|
||||
|
||||
cfs << CustomField.create!(name: "CF DEV required text",
|
||||
type: 'WorkPackageCustomField',
|
||||
is_required: true,
|
||||
field_format: 'text')
|
||||
print_status '.'
|
||||
|
||||
cfs << CustomField.create!(name: "CF DEV intrange",
|
||||
type: 'WorkPackageCustomField',
|
||||
min_length: 2,
|
||||
max_length: 5,
|
||||
field_format: 'int')
|
||||
print_status '.'
|
||||
|
||||
cfs
|
||||
end
|
||||
|
||||
@@ -28,18 +28,18 @@ module DevelopmentData
|
||||
class ProjectsSeeder < Seeder
|
||||
def seed_data!
|
||||
# We are relying on the default_projects_modules setting to set the desired project modules
|
||||
puts ' ↳ Creating development projects...'
|
||||
print_status ' ↳ Creating development projects...'
|
||||
|
||||
puts ' -Creating/Resetting development projects'
|
||||
print_status ' -Creating/Resetting development projects'
|
||||
projects = reset_projects
|
||||
|
||||
puts ' -Setting members.'
|
||||
print_status ' -Setting members.'
|
||||
set_members(projects)
|
||||
|
||||
puts ' -Creating versions.'
|
||||
print_status ' -Creating versions.'
|
||||
seed_versions(projects)
|
||||
|
||||
puts ' -Linking custom fields.'
|
||||
print_status ' -Linking custom fields.'
|
||||
|
||||
link_custom_fields(projects.detect { |p| p.identifier == 'dev-custom-fields' })
|
||||
end
|
||||
@@ -68,24 +68,24 @@ module DevelopmentData
|
||||
|
||||
def set_members(projects)
|
||||
%w(reader member project_admin).each do |id|
|
||||
user = User.find_by!(login: id)
|
||||
principal = User.find_by!(login: id)
|
||||
role = Role.find_by!(name: I18n.t("default_role_#{id}"))
|
||||
|
||||
projects.each { |p| Member.create! project: p, user:, roles: [role] }
|
||||
projects.each { |p| Member.create! project: p, principal:, roles: [role] }
|
||||
end
|
||||
end
|
||||
|
||||
def seed_versions(projects)
|
||||
projects.each do |p|
|
||||
version_data = project_data_for('scrum-project', 'versions')
|
||||
if version_data.is_a? Array
|
||||
version_data.each do |attributes|
|
||||
p.versions.create!(
|
||||
name: attributes[:name],
|
||||
status: attributes[:status],
|
||||
sharing: attributes[:sharing]
|
||||
)
|
||||
end
|
||||
version_data = seed_data.lookup('projects.scrum-project.versions')
|
||||
return unless version_data.is_a? Array
|
||||
|
||||
projects.each do |project|
|
||||
version_data.each do |attributes|
|
||||
project.versions.create!(
|
||||
name: attributes['name'],
|
||||
status: attributes['status'],
|
||||
sharing: attributes['sharing']
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
module DevelopmentData
|
||||
class UsersSeeder < Seeder
|
||||
def seed_data!
|
||||
puts 'Seeding development users ...'
|
||||
print_status 'Seeding development users ...'
|
||||
user_names.each do |login|
|
||||
user = new_user login.to_s
|
||||
|
||||
@@ -64,9 +64,9 @@ module DevelopmentData
|
||||
end
|
||||
|
||||
unless user.save! validate: false
|
||||
puts "Seeding #{login} user failed:"
|
||||
print_status "Seeding #{login} user failed:"
|
||||
user.errors.full_messages.each do |msg|
|
||||
puts " #{msg}"
|
||||
print_status " #{msg}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +31,6 @@ class DevelopmentDataSeeder < CompositeSeeder
|
||||
DevelopmentData::UsersSeeder,
|
||||
DevelopmentData::CustomFieldsSeeder,
|
||||
DevelopmentData::ProjectsSeeder
|
||||
# DevelopmentData::WorkPackageSeeder
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
+65
-38
@@ -30,9 +30,8 @@
|
||||
# as well as optional demo data (DemoDataSeeder) to give a user some orientation.
|
||||
|
||||
class RootSeeder < Seeder
|
||||
include Redmine::I18n
|
||||
|
||||
def initialize(seed_development_data: Rails.env.development?)
|
||||
super()
|
||||
require 'basic_data_seeder'
|
||||
require 'demo_data_seeder'
|
||||
require 'development_data_seeder'
|
||||
@@ -42,33 +41,34 @@ class RootSeeder < Seeder
|
||||
rails_engines.each { |engine| load_engine_seeders! engine }
|
||||
end
|
||||
|
||||
# Returns the demo data in the default language.
|
||||
def seed_data
|
||||
raise 'cannot generate demo seed data without setting locale first' unless @locale_set
|
||||
|
||||
@seed_data ||=
|
||||
OpenProject::Configuration['edition']
|
||||
.then { |edition| edition == 'bim' ? 'modules/bim/app/seeders/bim.yml' : 'app/seeders/standard.yml' }
|
||||
.then { |path| YAML.load_file(Rails.root.join(path)) }
|
||||
.then { |yaml_content| SeedData.new(yaml_content) }
|
||||
end
|
||||
|
||||
def seed_data!
|
||||
reset_active_record!
|
||||
set_locale!
|
||||
prepare_seed!
|
||||
|
||||
do_seed!
|
||||
set_locale! do
|
||||
prepare_seed! do
|
||||
do_seed!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def do_seed!
|
||||
ActiveRecord::Base.transaction do
|
||||
# Basic data needs be seeded before anything else.
|
||||
seed_basic_data
|
||||
|
||||
puts '*** Seeding admin user'
|
||||
AdminUserSeeder.new.seed!
|
||||
|
||||
puts '*** Seeding demo data'
|
||||
DemoDataSeeder.new.seed!
|
||||
|
||||
if seed_development_data?
|
||||
seed_development_data
|
||||
end
|
||||
|
||||
rails_engines.each do |engine|
|
||||
puts "*** Loading #{engine.engine_name} seed data"
|
||||
engine.load_seed
|
||||
end
|
||||
seed_admin_user
|
||||
seed_demo_data
|
||||
seed_development_data if seed_development_data?
|
||||
seed_plugins_data
|
||||
end
|
||||
end
|
||||
|
||||
@@ -98,32 +98,50 @@ class RootSeeder < Seeder
|
||||
end
|
||||
|
||||
def set_locale!
|
||||
# willfully ignoring Redmine::I18n and it's
|
||||
# #set_language_if_valid here as it
|
||||
# would mean to circumvent the default settings
|
||||
# for valid_languages.
|
||||
desired_lang = ENV.fetch('OPENPROJECT_SEED_LOCALE', :en).to_sym
|
||||
|
||||
if all_languages.include?(desired_lang)
|
||||
I18n.locale = desired_lang
|
||||
puts "*** Seeding for locale: '#{I18n.locale}'"
|
||||
else
|
||||
raise "Locale #{desired_lang} is not supported"
|
||||
end
|
||||
previous_locale = I18n.locale
|
||||
I18n.locale = desired_lang
|
||||
print_status "*** Seeding for locale: '#{I18n.locale}'"
|
||||
@locale_set = true
|
||||
yield
|
||||
ensure
|
||||
I18n.locale = previous_locale
|
||||
@locale_set = false
|
||||
end
|
||||
|
||||
def prepare_seed!
|
||||
# Disable mail delivery for the duration of this task
|
||||
previous_perform_deliveries = ActionMailer::Base.perform_deliveries
|
||||
ActionMailer::Base.perform_deliveries = false
|
||||
|
||||
# Avoid asynchronous DeliverWorkPackageCreatedJob
|
||||
previous_delay_jobs = Delayed::Worker.delay_jobs
|
||||
Delayed::Worker.delay_jobs = false
|
||||
|
||||
yield
|
||||
ensure
|
||||
ActionMailer::Base.perform_deliveries = previous_perform_deliveries
|
||||
Delayed::Worker.delay_jobs = previous_delay_jobs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seed_basic_data
|
||||
print_status "*** Seeding basic data for #{OpenProject::Configuration['edition']} edition"
|
||||
::Standard::BasicDataSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_admin_user
|
||||
print_status '*** Seeding admin user'
|
||||
AdminUserSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_demo_data
|
||||
print_status '*** Seeding demo data'
|
||||
DemoDataSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_development_data
|
||||
puts '*** Seeding development data'
|
||||
print_status '*** Seeding development data'
|
||||
require 'factory_bot'
|
||||
# Load FactoryBot factories
|
||||
begin
|
||||
@@ -132,11 +150,20 @@ class RootSeeder < Seeder
|
||||
raise e unless e.message.downcase.include? "factory already registered"
|
||||
end
|
||||
|
||||
DevelopmentDataSeeder.new.seed!
|
||||
DevelopmentDataSeeder.new(seed_data).seed!
|
||||
end
|
||||
|
||||
def seed_basic_data
|
||||
puts "*** Seeding basic data for #{OpenProject::Configuration['edition']} edition"
|
||||
::StandardSeeder::BasicDataSeeder.new.seed!
|
||||
def seed_plugins_data
|
||||
rails_engines.each do |engine|
|
||||
print_status "*** Loading #{engine.engine_name} seed data"
|
||||
engine.load_seed
|
||||
end
|
||||
end
|
||||
|
||||
def desired_lang
|
||||
desired_lang = ENV.fetch('OPENPROJECT_SEED_LOCALE', :en).to_sym
|
||||
raise "Locale #{desired_lang} is not supported" if Redmine::I18n.all_languages.exclude?(desired_lang)
|
||||
|
||||
desired_lang
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class SeedData
|
||||
def initialize(data)
|
||||
@data = data.deep_stringify_keys
|
||||
end
|
||||
|
||||
def lookup(path)
|
||||
case sub_data = fetch(path)
|
||||
when Hash
|
||||
SeedData.new(sub_data)
|
||||
else
|
||||
sub_data
|
||||
end
|
||||
end
|
||||
|
||||
def each(path, &)
|
||||
case sub_data = fetch(path)
|
||||
when nil
|
||||
nil
|
||||
when Enumerable
|
||||
sub_data.each(&)
|
||||
else
|
||||
raise ArgumentError, "expected an Enumerable at path #{path}, got #{sub_data.class}"
|
||||
end
|
||||
end
|
||||
|
||||
def each_data(path)
|
||||
sub_data = fetch(path)
|
||||
return if sub_data.nil?
|
||||
|
||||
sub_data.each_value do |item_data|
|
||||
yield SeedData.new(item_data)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch(path)
|
||||
keys = path.to_s.split('.')
|
||||
@data.dig(*keys)
|
||||
end
|
||||
end
|
||||
+31
-23
@@ -27,13 +27,35 @@
|
||||
#++
|
||||
|
||||
class Seeder
|
||||
class << self
|
||||
attr_writer :logger
|
||||
|
||||
def logger
|
||||
@logger ||= Rails.logger
|
||||
end
|
||||
|
||||
def log_to_stdout!
|
||||
@logger = Logger.new($stdout)
|
||||
@logger.level = Logger::DEBUG
|
||||
@logger.formatter = proc do |_severity, _datetime, _prog_name, msg|
|
||||
"#{msg}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :seed_data
|
||||
|
||||
def initialize(seed_data = nil)
|
||||
@seed_data = seed_data
|
||||
end
|
||||
|
||||
def seed!
|
||||
if applicable?
|
||||
without_notifications do
|
||||
seed_data!
|
||||
end
|
||||
else
|
||||
Rails.logger.debug { " *** #{not_applicable_message}" }
|
||||
Seeder.logger.debug { " *** #{not_applicable_message}" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,35 +71,21 @@ class Seeder
|
||||
"Skipping #{self.class.name}"
|
||||
end
|
||||
|
||||
# The user being the author of all data created during seeding.
|
||||
def user
|
||||
@user ||= User.not_builtin.admin.first
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def print_status(message)
|
||||
Rails.logger.info message
|
||||
Seeder.logger.info message
|
||||
|
||||
yield if block_given?
|
||||
end
|
||||
|
||||
##
|
||||
# Translate the given string with the fixed interpolation for base_url
|
||||
# Deep interpolation is required in order for interpolations on hashes to work!
|
||||
def translate_with_base_url(string, **i18n_options)
|
||||
I18n.t(string, deep_interpolation: true, base_url: "{{opSetting:base_url}}", **i18n_options)
|
||||
end
|
||||
|
||||
def edition_data_for(key)
|
||||
translate_with_base_url("seeders.#{OpenProject::Configuration['edition']}.#{key}", default: nil)
|
||||
end
|
||||
|
||||
def demo_data_for(key)
|
||||
edition_data_for("demo_data.#{key}")
|
||||
end
|
||||
|
||||
def project_data_for(project, key)
|
||||
demo_data_for "projects.#{project}.#{key}"
|
||||
end
|
||||
|
||||
def project_has_data_for?(project, key)
|
||||
I18n.exists?("seeders.#{OpenProject::Configuration['edition']}.demo_data.projects.#{project}.#{key}")
|
||||
def print_error(message)
|
||||
Seeder.logger.error message
|
||||
end
|
||||
|
||||
def without_notifications(&)
|
||||
|
||||
@@ -0,0 +1,705 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
welcome:
|
||||
title: "Welcome to OpenProject!"
|
||||
text: |
|
||||
Select one of the demo projects to get started with some demo data we have prepared for you.
|
||||
|
||||
* [Demo project]({{opSetting:base_url}}/projects/demo-project): to get an overview about classical project management.
|
||||
* [Scrum project]({{opSetting:base_url}}/projects/your-scrum-project): to learn about Agile and Scrum project management.
|
||||
|
||||
Also, you can create a blank [new project]({{opSetting:base_url}}/projects/new).
|
||||
|
||||
Never stop collaborating. With open source and open mind.
|
||||
|
||||
You can change this welcome text [here]({{opSetting:base_url}}/admin/settings/general).
|
||||
projects:
|
||||
demo-project:
|
||||
name: Demo project
|
||||
identifier: demo-project
|
||||
status:
|
||||
code: on_track
|
||||
description: All tasks are on schedule. The people involved know their tasks. The system is completely set up.
|
||||
description: This is a short summary of the goals of this demo project.
|
||||
timeline:
|
||||
name: Timeline
|
||||
modules:
|
||||
- work_package_tracking
|
||||
- news
|
||||
- wiki
|
||||
- board_view
|
||||
- team_planner_view
|
||||
news:
|
||||
- title: Welcome to your demo project
|
||||
summary: >
|
||||
We are glad you joined.
|
||||
In this module you can communicate project news to your team members.
|
||||
description: The actual news
|
||||
types:
|
||||
- :default_type_task
|
||||
- :default_type_milestone
|
||||
- :default_type_phase
|
||||
categories:
|
||||
- Category 1 (to be changed in Project settings)
|
||||
queries:
|
||||
- name: Project plan
|
||||
status: open
|
||||
timeline: true
|
||||
sort_by: id
|
||||
hierarchy: true
|
||||
starred: true
|
||||
columns:
|
||||
- id
|
||||
- type
|
||||
- subject
|
||||
- status
|
||||
- start_date
|
||||
- due_date
|
||||
- duration
|
||||
- assigned_to
|
||||
- name: Milestones
|
||||
type: :default_type_milestone
|
||||
timeline: true
|
||||
columns:
|
||||
- id
|
||||
- type
|
||||
- subject
|
||||
- status
|
||||
- start_date
|
||||
- due_date
|
||||
sort_by: id
|
||||
- name: Tasks
|
||||
status: open
|
||||
type: :default_type_task
|
||||
hierarchy: true
|
||||
sort_by: id
|
||||
columns:
|
||||
- id
|
||||
- subject
|
||||
- priority
|
||||
- status
|
||||
- assigned_to
|
||||
- name: Team planner
|
||||
module: team_planner
|
||||
assignee: OpenProject Admin
|
||||
boards:
|
||||
kanban:
|
||||
name: 'Kanban board'
|
||||
filters:
|
||||
- type: default_type_task
|
||||
basic:
|
||||
name: 'Basic board'
|
||||
parent_child:
|
||||
name: 'Work breakdown structure'
|
||||
project-overview:
|
||||
row_count: 6
|
||||
column_count: 2
|
||||
widgets:
|
||||
- identifier: 'custom_text'
|
||||
start_row: 1
|
||||
end_row: 2
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Welcome'
|
||||
text: ''
|
||||
attachments:
|
||||
- demo_project_teaser.png
|
||||
- identifier: 'custom_text'
|
||||
start_row: 2
|
||||
end_row: 5
|
||||
start_column: 1
|
||||
end_column: 2
|
||||
options:
|
||||
name: 'Getting started'
|
||||
text: |
|
||||
We are glad you joined! We suggest to try a few things to get started in OpenProject.
|
||||
|
||||
Discover the most important features with our [Guided Tour]({{opSetting:base_url}}/projects/demo-project/work_packages/?start_onboarding_tour=true).
|
||||
|
||||
_Try the following steps:_
|
||||
|
||||
1. *Invite new members to your project*: → Go to [Members]({{opSetting:base_url}}/projects/demo-project/members) in the project navigation.
|
||||
2. *View the work in your project*: → Go to [Work packages]({{opSetting:base_url}}/projects/demo-project/work_packages) in the project navigation.
|
||||
3. *Create a new work package*: → Go to [Work packages → Create]({{opSetting:base_url}}/projects/demo-project/work_packages/new).
|
||||
4. *Create and update a project plan*: → Go to [Project plan]({{opSetting:base_url}}/projects/demo-project/work_packages?query_id=##query.id:"Project plan") in the project navigation.
|
||||
5. *Activate further modules*: → Go to [Project settings → Modules]({{opSetting:base_url}}/projects/demo-project/settings/modules).
|
||||
6. *Complete your tasks in the project*: → Go to [Work packages → Tasks]({{opSetting:base_url}}/projects/demo-project/work_packages/details/##wp.id:"Set date and location of conference"/overview?query_id=##query.id:"Tasks").
|
||||
|
||||
Here you will find our [User Guides](https://www.openproject.org/docs/user-guide/).
|
||||
Please let us know if you have any questions or need support. Contact us: [support[at]openproject.com](mailto:support@openproject.com).
|
||||
- identifier: 'project_status'
|
||||
start_row: 2
|
||||
end_row: 3
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
- identifier: 'project_description'
|
||||
start_row: 3
|
||||
end_row: 4
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
- identifier: 'members'
|
||||
start_row: 4
|
||||
end_row: 5
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Members'
|
||||
- identifier: 'work_packages_overview'
|
||||
start_row: 5
|
||||
end_row: 6
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Work packages'
|
||||
- identifier: 'work_packages_table'
|
||||
start_row: 6
|
||||
end_row: 7
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Milestones'
|
||||
queryId: '##query.id:"Milestones"'
|
||||
# For all dates, the reference is the monday of the current week
|
||||
# So
|
||||
# * start: 0 references Monday
|
||||
# * start: 4 references Friday
|
||||
# * start: -1 references the last Sunday
|
||||
work_packages:
|
||||
- start: -1
|
||||
subject: Start of project
|
||||
description:
|
||||
status: default_status_closed
|
||||
type: default_type_milestone
|
||||
duration: 1
|
||||
schedule_manually: false
|
||||
- start: 0
|
||||
subject: Organize open source conference
|
||||
description:
|
||||
status: default_status_in_progress
|
||||
type: default_type_phase
|
||||
children:
|
||||
- start: 0
|
||||
subject: Set date and location of conference
|
||||
description: ''
|
||||
status: default_status_in_progress
|
||||
type: default_type_task
|
||||
children:
|
||||
- start: 0
|
||||
subject: Send invitation to speakers
|
||||
description: ''
|
||||
status: default_status_in_progress
|
||||
type: default_type_task
|
||||
duration: 1
|
||||
- start: 0
|
||||
subject: Contact sponsoring partners
|
||||
description: ''
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 2
|
||||
- start: 0
|
||||
subject: Create sponsorship brochure and hand-outs
|
||||
description: ''
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 4
|
||||
duration: 4
|
||||
schedule_manually: false
|
||||
- start: 4
|
||||
subject: Invite attendees to conference
|
||||
description: ''
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Set date and location of conference
|
||||
type: follows
|
||||
schedule_manually: false
|
||||
- start: 4
|
||||
subject: Setup conference website
|
||||
description: ''
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 11
|
||||
relations:
|
||||
- to: Set date and location of conference
|
||||
type: follows
|
||||
schedule_manually: false
|
||||
duration: 15
|
||||
schedule_manually: true
|
||||
- start: 15
|
||||
subject: Conference
|
||||
description:
|
||||
status: default_status_scheduled
|
||||
type: default_type_milestone
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Organize open source conference
|
||||
type: follows
|
||||
schedule_manually: false
|
||||
- start: 21
|
||||
subject: Follow-up tasks
|
||||
description:
|
||||
status: default_status_to_be_scheduled
|
||||
type: default_type_phase
|
||||
children:
|
||||
- start: 21
|
||||
subject: Upload presentations to website
|
||||
description: ''
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 10
|
||||
schedule_manually: false
|
||||
- start: 31
|
||||
subject: Party for conference supporters :-)
|
||||
description: |-
|
||||
* [ ] Beer
|
||||
* [ ] Snacks
|
||||
* [ ] Music
|
||||
* [ ] Even more beer
|
||||
status: default_status_new
|
||||
type: default_type_task
|
||||
duration: 1
|
||||
schedule_manually: false
|
||||
duration: 11
|
||||
schedule_manually: false
|
||||
- start: 32
|
||||
subject: End of project
|
||||
description:
|
||||
status: default_status_new
|
||||
type: default_type_milestone
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Follow-up tasks
|
||||
type: follows
|
||||
schedule_manually: false
|
||||
scrum-project:
|
||||
name: Scrum project
|
||||
identifier: your-scrum-project
|
||||
status:
|
||||
code: on_track
|
||||
description: All tasks are on schedule. The people involved know their tasks. The system is completely set up.
|
||||
description: This is a short summary of the goals of this demo Scrum project.
|
||||
timeline:
|
||||
name: Timeline
|
||||
modules:
|
||||
- backlogs
|
||||
- news
|
||||
- wiki
|
||||
- work_package_tracking
|
||||
- board_view
|
||||
news:
|
||||
- title: Welcome to your Scrum demo project
|
||||
summary: >
|
||||
We are glad you joined.
|
||||
In this module you can communicate project news to your team members.
|
||||
description: This is the news content.
|
||||
versions:
|
||||
- name: Bug Backlog
|
||||
sharing: none
|
||||
status: open
|
||||
- name: Product Backlog
|
||||
sharing: none
|
||||
status: open
|
||||
start: 15
|
||||
- name: Sprint 1
|
||||
sharing: none
|
||||
status: open
|
||||
start: 4
|
||||
duration: 7
|
||||
wiki:
|
||||
title: Sprint 1
|
||||
content: |
|
||||
### Sprint planning meeting
|
||||
|
||||
_Please document here topics to the Sprint planning meeting_
|
||||
|
||||
* Time boxed (8 h)
|
||||
* Input: Product Backlog
|
||||
* Output: Sprint Backlog
|
||||
|
||||
* Divided into two additional time boxes of 4 h:
|
||||
|
||||
* The Product Owner presents the [Product Backlog]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) and the priorities to the team and explains the Sprint Goal, to which the team must agree. Together, they prioritize the topics from the Product Backlog which the team will take care of in the next sprint. The team commits to the discussed delivery.
|
||||
* The team plans autonomously (without the Product Owner) in detail and breaks down the tasks from the discussed requirements to consolidate a [Sprint Backlog]({{opSetting:base_url}}/projects/your-scrum-project/backlogs).
|
||||
|
||||
|
||||
### Daily Scrum meeting
|
||||
|
||||
_Please document here topics to the Daily Scrum meeting_
|
||||
|
||||
* Short, daily status meeting of the team.
|
||||
* Time boxed (max. 15 min).
|
||||
* Stand-up meeting to discuss the following topics from the [Task board](##sprint:"Sprint 1").
|
||||
* What do I plan to do until the next Daily Scrum?
|
||||
* What has blocked my work (Impediments)?
|
||||
* Scrum Master moderates and notes down [Sprint Impediments](##sprint:"Sprint 1").
|
||||
* Product Owner may participate may participate in order to stay informed.
|
||||
|
||||
### Sprint Review meeting
|
||||
|
||||
_Please document here topics to the Sprint Review meeting_
|
||||
|
||||
* Time boxed (4 h).
|
||||
* A maximum of one hour of preparation time per person.
|
||||
* The team shows the product owner and other interested persons what has been achieved in this sprint.
|
||||
* Important: no dummies and no PowerPoint! Just finished product functionality (Increments) should be demonstrated.
|
||||
* Feedback from Product Owner, stakeholders and others is desired and will be included in further work.
|
||||
* Based on the demonstrated functionalities, the Product Owner decides to go live with this increment or to develop it further. This possibility allows an early ROI.
|
||||
|
||||
|
||||
### Sprint Retrospective
|
||||
|
||||
_Please document here topics to the Sprint Retrospective meeting_
|
||||
|
||||
* Time boxed (3 h).
|
||||
* After Sprint Review, will be moderated by Scrum Master.
|
||||
* The team discusses the sprint: what went well, what needs to be improved to be more productive for the next sprint or even have more fun.
|
||||
- name: Sprint 2
|
||||
sharing: none
|
||||
status: open
|
||||
types:
|
||||
- :default_type_task
|
||||
- :default_type_milestone
|
||||
- :default_type_phase
|
||||
- :default_type_epic
|
||||
- :default_type_user_story
|
||||
- :default_type_bug
|
||||
categories:
|
||||
- Category 1 (to be changed in Project settings)
|
||||
queries:
|
||||
- name: Project plan
|
||||
status: open
|
||||
sort_by: id
|
||||
type:
|
||||
- :default_type_milestone
|
||||
- :default_type_phase
|
||||
timeline: true
|
||||
- name: Product backlog
|
||||
status: open
|
||||
version: Product Backlog
|
||||
group_by: status
|
||||
sort_by: status
|
||||
columns:
|
||||
- id
|
||||
- type
|
||||
- subject
|
||||
- priority
|
||||
- status
|
||||
- assigned_to
|
||||
- story_points
|
||||
- name: Sprint 1
|
||||
status: open
|
||||
version: Sprint 1
|
||||
hierarchy: true
|
||||
columns:
|
||||
- id
|
||||
- type
|
||||
- subject
|
||||
- priority
|
||||
- status
|
||||
- assigned_to
|
||||
- done_ratio
|
||||
- story_points
|
||||
- name: Tasks
|
||||
status: open
|
||||
type: :default_type_task
|
||||
hierarchy: true
|
||||
boards:
|
||||
kanban:
|
||||
name: 'Kanban board'
|
||||
basic:
|
||||
name: 'Task board'
|
||||
project-overview:
|
||||
row_count: 6
|
||||
column_count: 2
|
||||
widgets:
|
||||
- identifier: 'custom_text'
|
||||
start_row: 1
|
||||
end_row: 2
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Welcome'
|
||||
text: ''
|
||||
attachments:
|
||||
- scrum_project_teaser.png
|
||||
- identifier: 'custom_text'
|
||||
start_row: 2
|
||||
end_row: 5
|
||||
start_column: 1
|
||||
end_column: 2
|
||||
options:
|
||||
name: 'Getting started'
|
||||
text: |
|
||||
We are glad you joined! We suggest to try a few things to get started in OpenProject.
|
||||
|
||||
Discover the most important features with our [Guided Tour]({{opSetting:base_url}}/projects/your-scrum-project/backlogs?start_scrum_onboarding_tour=true).
|
||||
|
||||
_Try the following steps:_
|
||||
|
||||
1. *Invite new members to your project*: → Go to [Members]({{opSetting:base_url}}/projects/your-scrum-project/members) in the project navigation.
|
||||
2. *View your Product backlog and Sprint backlogs*: → Go to [Backlogs]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) in the project navigation.
|
||||
3. *View your Task board*: → Go to [Backlogs]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) → Click on right arrow on Sprint → Select [Task Board](##sprint:"Sprint 1").
|
||||
4. *Create a new work package*: → Go to [Work packages → Create]({{opSetting:base_url}}/projects/your-scrum-project/work_packages/new).
|
||||
5. *Create and update a project plan*: → Go to [Project plan](##query:"Project plan") in the project navigation.
|
||||
6. *Create a Sprint wiki*: → Go to [Backlogs]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) and open the sprint wiki from the right drop down menu in a sprint. You can edit the [wiki template]({{opSetting:base_url}}/projects/your-scrum-project/wiki/) based on your needs.
|
||||
7. *Activate further modules*: → Go to [Project settings → Modules]({{opSetting:base_url}}/projects/your-scrum-project/settings/modules).
|
||||
|
||||
Here you will find our [User Guides](https://www.openproject.org/docs/user-guide/).
|
||||
Please let us know if you have any questions or need support. Contact us: [support[at]openproject.com](mailto:support@openproject.com).
|
||||
- identifier: 'project_status'
|
||||
start_row: 2
|
||||
end_row: 3
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
- identifier: 'project_description'
|
||||
start_row: 3
|
||||
end_row: 4
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
- identifier: 'members'
|
||||
start_row: 4
|
||||
end_row: 5
|
||||
start_column: 2
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Members'
|
||||
- identifier: 'work_packages_overview'
|
||||
start_row: 5
|
||||
end_row: 6
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Work packages'
|
||||
- identifier: 'work_packages_table'
|
||||
start_row: 6
|
||||
end_row: 7
|
||||
start_column: 1
|
||||
end_column: 3
|
||||
options:
|
||||
name: 'Project plan'
|
||||
queryId: '##query.id:"Project plan"'
|
||||
work_packages:
|
||||
- subject: New login screen
|
||||
status: :default_status_in_specification
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 3
|
||||
- subject: Password reset does not send email
|
||||
status: :default_status_confirmed
|
||||
type: :default_type_bug
|
||||
version: Bug Backlog
|
||||
position: 1
|
||||
- subject: New website
|
||||
status: :default_status_specified
|
||||
type: :default_type_epic
|
||||
start: 0
|
||||
duration: 29
|
||||
children:
|
||||
- subject: Newsletter registration form
|
||||
status: :default_status_in_progress
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 6
|
||||
- subject: Implement product tour
|
||||
status: :default_status_in_specification
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 4
|
||||
- subject: New landing page
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Sprint 1
|
||||
position: 2
|
||||
story_points: 3
|
||||
start: 28
|
||||
duration: 1
|
||||
children:
|
||||
- subject: Create wireframes for new landing page
|
||||
status: :default_status_in_progress
|
||||
type: :default_type_task
|
||||
version: Sprint 1
|
||||
start: 28
|
||||
duration: 1
|
||||
- subject: Contact form
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Sprint 1
|
||||
position: 5
|
||||
start: 21
|
||||
duration: 1
|
||||
story_points: 1
|
||||
- subject: Feature carousel
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Sprint 1
|
||||
position: 3
|
||||
story_points: 5
|
||||
children:
|
||||
- subject: Make screenshots for feature tour
|
||||
status: :default_status_closed
|
||||
type: :default_type_task
|
||||
version: Sprint 1
|
||||
- subject: Wrong hover color
|
||||
status: :default_status_rejected
|
||||
type: :default_type_bug
|
||||
version: Sprint 1
|
||||
position: 4
|
||||
story_points: 1
|
||||
start: 21
|
||||
duration: 1
|
||||
- subject: SSL certificate
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 1
|
||||
start: 22
|
||||
duration: 1
|
||||
- subject: Set-up Staging environment
|
||||
status: :default_status_in_specification
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 2
|
||||
start: 23
|
||||
duration: 1
|
||||
- subject: Choose a content management system
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 7
|
||||
start: 24
|
||||
duration: 1
|
||||
- subject: Website navigation structure
|
||||
status: :default_status_specified
|
||||
type: :default_type_user_story
|
||||
version: Sprint 1
|
||||
position: 7
|
||||
story_points: 3
|
||||
start: 25
|
||||
duration: 1
|
||||
children:
|
||||
- subject: Set up navigation concept for website.
|
||||
status: :default_status_in_specification
|
||||
type: :default_type_task
|
||||
version: Sprint 1
|
||||
start: 25
|
||||
duration: 1
|
||||
- subject: Internal link structure
|
||||
status: :default_status_closed
|
||||
type: :default_type_user_story
|
||||
version: Product Backlog
|
||||
position: 5
|
||||
start: 25
|
||||
duration: 1
|
||||
- subject: Develop v1.0
|
||||
status: :default_status_in_progress
|
||||
type: :default_type_phase
|
||||
start: 14
|
||||
duration: 3
|
||||
- subject: Release v1.0
|
||||
status: :default_status_new
|
||||
type: :default_type_milestone
|
||||
start: 18
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Develop v1.0
|
||||
type: follows
|
||||
- subject: Develop v1.1
|
||||
status: :default_status_new
|
||||
type: :default_type_phase
|
||||
start: 21
|
||||
duration: 3
|
||||
- subject: Release v1.1
|
||||
status: :default_status_new
|
||||
type: :default_type_milestone
|
||||
start: 25
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Develop v1.1
|
||||
type: follows
|
||||
- subject: Develop v2.0
|
||||
status: :default_status_new
|
||||
type: :default_type_phase
|
||||
start: 28
|
||||
duration: 3
|
||||
- subject: Release v2.0
|
||||
status: :default_status_new
|
||||
type: :default_type_milestone
|
||||
start: 32
|
||||
duration: 1
|
||||
relations:
|
||||
- to: Develop v2.0
|
||||
type: follows
|
||||
wiki: |
|
||||
### Sprint planning meeting
|
||||
|
||||
_Please document here topics to the Sprint planning meeting_
|
||||
|
||||
* Time boxed (8 h)
|
||||
* Input: Product Backlog
|
||||
* Output: Sprint Backlog
|
||||
|
||||
* Divided into two additional time boxes of 4 h:
|
||||
|
||||
* The Product Owner presents the [Product Backlog]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) and the priorities to the team and explains the Sprint Goal, to which the team must agree. Together, they prioritize the topics from the Product Backlog which the team will take care of in the next sprint. The team commits to the discussed delivery.
|
||||
* The team plans autonomously (without the Product Owner) in detail and breaks down the tasks from the discussed requirements to consolidate a [Sprint Backlog]({{opSetting:base_url}}/projects/your-scrum-project/backlogs).
|
||||
|
||||
|
||||
### Daily Scrum meeting
|
||||
|
||||
_Please document here topics to the Daily Scrum meeting_
|
||||
|
||||
* Short, daily status meeting of the team.
|
||||
* Time boxed (max. 15 min).
|
||||
* Stand-up meeting to discuss the following topics from the Task board.
|
||||
* What do I plan to do until the next Daily Scrum?
|
||||
* What has blocked my work (Impediments)?
|
||||
* Scrum Master moderates and notes down Sprint Impediments.
|
||||
* Product Owner may participate may participate in order to stay informed.
|
||||
|
||||
### Sprint Review meeting
|
||||
|
||||
_Please document here topics to the Sprint Review meeting_
|
||||
|
||||
* Time boxed (4 h).
|
||||
* A maximum of one hour of preparation time per person.
|
||||
* The team shows the product owner and other interested persons what has been achieved in this sprint.
|
||||
* Important: no dummies and no PowerPoint! Just finished product functionality (Increments) should be demonstrated.
|
||||
* Feedback from Product Owner, stakeholders and others is desired and will be included in further work.
|
||||
* Based on the demonstrated functionalities, the Product Owner decides to go live with this increment or to develop it further. This possibility allows an early ROI.
|
||||
|
||||
|
||||
### Sprint Retrospective
|
||||
|
||||
_Please document here topics to the Sprint Retrospective meeting_
|
||||
|
||||
* Time boxed (3 h).
|
||||
* After Sprint Review, will be moderated by Scrum Master.
|
||||
* The team discusses the sprint: what went well, what needs to be improved to be more productive for the next sprint or even have more fun.
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
module BasicData
|
||||
class ActivitySeeder < ::BasicData::ActivitySeeder
|
||||
def data
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
module BasicData
|
||||
class PrioritySeeder < ::BasicData::PrioritySeeder
|
||||
def data
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
module BasicData
|
||||
class StatusSeeder < ::BasicData::StatusSeeder
|
||||
def data
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
module BasicData
|
||||
class TypeSeeder < ::BasicData::TypeSeeder
|
||||
def type_names
|
||||
+3
-3
@@ -25,7 +25,7 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
module BasicData
|
||||
class WorkflowSeeder < ::BasicData::WorkflowSeeder
|
||||
def workflows
|
||||
@@ -63,11 +63,11 @@ module StandardSeeder
|
||||
end
|
||||
|
||||
def type_seeder_class
|
||||
::StandardSeeder::BasicData::TypeSeeder
|
||||
::Standard::BasicData::TypeSeeder
|
||||
end
|
||||
|
||||
def status_seeder_class
|
||||
::StandardSeeder::BasicData::StatusSeeder
|
||||
::Standard::BasicData::StatusSeeder
|
||||
end
|
||||
end
|
||||
end
|
||||
+5
-4
@@ -25,17 +25,18 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module StandardSeeder
|
||||
module Standard
|
||||
class BasicDataSeeder < ::BasicDataSeeder
|
||||
def data_seeder_classes
|
||||
[
|
||||
::BasicData::BuiltinUsersSeeder,
|
||||
::BasicData::BuiltinRolesSeeder,
|
||||
::BasicData::RoleSeeder,
|
||||
::StandardSeeder::BasicData::ActivitySeeder,
|
||||
::Standard::BasicData::ActivitySeeder,
|
||||
::BasicData::ColorSeeder,
|
||||
::BasicData::ColorSchemeSeeder,
|
||||
::StandardSeeder::BasicData::WorkflowSeeder,
|
||||
::StandardSeeder::BasicData::PrioritySeeder,
|
||||
::Standard::BasicData::WorkflowSeeder,
|
||||
::Standard::BasicData::PrioritySeeder,
|
||||
::BasicData::SettingSeeder
|
||||
]
|
||||
end
|
||||
@@ -162,7 +162,7 @@ module Authentication
|
||||
def remap_existing_user
|
||||
return unless Setting.oauth_allow_remapping_of_existing_users?
|
||||
|
||||
User.find_by_login(user_attributes[:login])
|
||||
User.not_builtin.find_by(login: user_attributes[:login])
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -40,7 +40,6 @@ class Authorization::EnterpriseService
|
||||
edit_attribute_groups
|
||||
grid_widget_wp_graph
|
||||
ldap_groups
|
||||
multiselect_custom_fields
|
||||
openid_providers
|
||||
placeholder_users
|
||||
readonly_work_packages
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
# Purpose: create and persist a Storages::Storage record
|
||||
# Used by: Storages::Admin::StoragesController#create, could also be used by the
|
||||
# API in the future.
|
||||
# Reference: https://www.openproject.org/docs/development/concepts/contracted-services/
|
||||
# The comments here are also valid for the other *_service.rb files
|
||||
module OAuthClients
|
||||
class CreateService < ::BaseServices::Create
|
||||
|
||||
@@ -39,8 +39,8 @@ module Projects
|
||||
private
|
||||
|
||||
def persist(service_call)
|
||||
archive_project(model) and model.children.each do |child|
|
||||
archive_project(child)
|
||||
archive_project(model) and model.active_subprojects.each do |subproject|
|
||||
archive_project(subproject)
|
||||
end
|
||||
|
||||
service_call
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This 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 COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
require_relative './base_service'
|
||||
|
||||
module Sessions
|
||||
class DropAllSessionsService < BaseService
|
||||
class << self
|
||||
##
|
||||
# Drop all sessions for the given user
|
||||
def call(user)
|
||||
return false unless active_record_sessions?
|
||||
|
||||
::Sessions::UserSession
|
||||
.for_user(user)
|
||||
.delete_all
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -42,6 +42,8 @@ module Users
|
||||
current_user.force_password_change = false
|
||||
|
||||
if current_user.save
|
||||
invalidate_recovery_tokens
|
||||
|
||||
log_success
|
||||
::ServiceResult.new success: true,
|
||||
result: current_user,
|
||||
@@ -58,6 +60,10 @@ module Users
|
||||
|
||||
private
|
||||
|
||||
def invalidate_recovery_tokens
|
||||
Token::Recovery.where(user: current_user).delete_all
|
||||
end
|
||||
|
||||
def invalidate_session_result
|
||||
update_message = I18n.t(:notice_account_password_updated)
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ module Users
|
||||
# Try to register a user with an existsing omniauth connection
|
||||
# bypassing regular account registration restrictions
|
||||
def register_omniauth_user
|
||||
return if user.identity_url.blank?
|
||||
return if skip_omniauth_user?
|
||||
|
||||
user.activate
|
||||
|
||||
@@ -113,6 +113,10 @@ module Users
|
||||
end
|
||||
end
|
||||
|
||||
def skip_omniauth_user?
|
||||
user.identity_url.blank?
|
||||
end
|
||||
|
||||
def register_by_email_activation
|
||||
return unless Setting::SelfRegistration.by_email?
|
||||
|
||||
|
||||
@@ -42,13 +42,21 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<div style="float:left;">
|
||||
<%= link_to_content_update(t(:label_previous),
|
||||
{ from: (@date_to - @days - 1), with_subprojects: @with_subprojects ? '1' : '0' },
|
||||
{
|
||||
from: (@date_to - @days - 1),
|
||||
with_subprojects: @with_subprojects ? '1' : '0',
|
||||
user_id: params[:user_id]
|
||||
}.compact,
|
||||
{title: t(:label_date_from_to, start: format_date(@date_to - 2*@days), end: format_date(@date_to - @days - 1)),
|
||||
class: 'navigate-left'}) %>
|
||||
</div>
|
||||
<div style="float:right;">
|
||||
<%= link_to_content_update(t(:label_next),
|
||||
{ from: (@date_to + @days - 1), with_subprojects: @with_subprojects ? '1' : '0' },
|
||||
{
|
||||
from: (@date_to + @days - 1),
|
||||
with_subprojects: @with_subprojects ? '1' : '0',
|
||||
user_id: params[:user_id]
|
||||
}.compact,
|
||||
{title: t(:label_date_from_to, start: format_date(@date_to), end: format_date(@date_to + @days - 1)),
|
||||
class: 'navigate-right'}) unless @date_to >= Date.today %>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<div class="menu-blocks--container" data-qa-selector="menu-blocks--container">
|
||||
<% @menu_nodes.each do |menu_node| -%>
|
||||
<%= link_to menu_node.url, { class: 'menu-block', 'data-qa-selector': 'menu-block' } do %>
|
||||
<%= op_icon('menu-block--icon ' + (menu_node.icon || '').gsub(/icon2/, "icon3")) %>
|
||||
<%= op_icon("menu-block--icon icon3 icon-#{(menu_node.icon || '')}") %>
|
||||
<span class="menu-block--title"> <%= menu_node.caption %> </span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
aria: {label: t('admin.enterprise.buttons.upgrade')},
|
||||
target: '_blank',
|
||||
title: t('admin.enterprise.buttons.upgrade')}) do %>
|
||||
<%= spot_icon('enterprise-addons enterprise-addons--button-icon') %>
|
||||
<%= spot_icon('enterprise-addons') %>
|
||||
<span class="button--text"><%= t('admin.enterprise.buttons.upgrade') %></span>
|
||||
<% end %>
|
||||
<free-trial-button></free-trial-button>
|
||||
|
||||
@@ -61,27 +61,9 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
</div>
|
||||
|
||||
<% if @custom_field.new_record? || @custom_field.list? || @custom_field.multi_value_possible? %>
|
||||
<div class="form--field" id="custom_field_multi_select" style="display:none">
|
||||
<% if EnterpriseToken.allows_to?(:multiselect_custom_fields) %>
|
||||
<%= f.check_box :multi_value %>
|
||||
<% else %>
|
||||
<label class="form--label" for="custom_field_multi_value_disabled"><%= CustomField.human_attribute_name('multi_value') %></label>
|
||||
<span class="form--field-container">
|
||||
<span class="form--check-box-container">
|
||||
<input disabled="disabled" class="-cf-ignore-disabled form--check-box" type="checkbox" name="custom_field_multi_value_disabled">
|
||||
</span>
|
||||
</span>
|
||||
<div class="form--field-instructions -no-italic -xwide">
|
||||
<%= angular_component_tag 'op-enterprise-banner',
|
||||
inputs: {
|
||||
collapsible: true,
|
||||
textMessage: t('text_wp_custom_field_html'),
|
||||
moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:custom_field_multiselect][:href],
|
||||
}
|
||||
%>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form--field" id="custom_field_multi_select" style="display:none">
|
||||
<%= f.check_box :multi_value %>
|
||||
</div>
|
||||
|
||||
<fieldset class="form--fieldset" id="custom_field_possible_values_attributes">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("activerecord.attributes.custom_field.possible_values") %></legend>
|
||||
|
||||
@@ -40,19 +40,6 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if tab[:name] == 'WorkPackageCustomField' %>
|
||||
<% unless EnterpriseToken.allows_to?(:multiselect_custom_fields) %>
|
||||
<%=
|
||||
angular_component_tag 'op-enterprise-banner',
|
||||
inputs: {
|
||||
collapsible: true,
|
||||
textMessage: t('text_wp_custom_field_html'),
|
||||
moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:custom_field_multiselect][:href],
|
||||
}
|
||||
%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if (@custom_fields_by_type[tab[:name]] || []).any? %>
|
||||
<div class="generic-table--container">
|
||||
<div class="generic-table--results-container">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
aria: {label: t('admin.enterprise.buttons.upgrade')},
|
||||
target: '_blank',
|
||||
title: t('admin.enterprise.buttons.upgrade')}) do %>
|
||||
<%= spot_icon('enterprise-addons enterprise-addons--button-icon') %>
|
||||
<%= spot_icon('enterprise-addons') %>
|
||||
<span class="button--text"><%= t('admin.enterprise.buttons.upgrade') %></span>
|
||||
<% end %>
|
||||
|
||||
|
||||
+14
-2
@@ -57,12 +57,24 @@ elif [[ "$1" = "setup" ]]; then
|
||||
elif [[ "$1" = "reset" ]]; then
|
||||
$DOCKER_COMPOSE -f $COMPOSE_FILE down && docker volume rm `docker volume ls -q | grep ${PWD##*/}_`
|
||||
elif [[ "$1" = "rspec" ]]; then
|
||||
if ! docker ps | grep ${PWD##*/}_backend-test_1 > /dev/null; then
|
||||
function get-container-name() {
|
||||
name=`$DOCKER_COMPOSE ps backend-test | tail -n1 | cut -d ' ' -f1`
|
||||
|
||||
if [ "$name" = 'NAME' ]; then
|
||||
return 1;
|
||||
else
|
||||
echo "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! get-container-name > /dev/null; then
|
||||
echo "Test backend not running yet. Starting it..."
|
||||
|
||||
$DOCKER_COMPOSE -f $COMPOSE_FILE up -d backend-test
|
||||
|
||||
while ! docker logs --since 1m ${PWD##*/}_backend-test_1 | grep "Ready for tests" > /dev/null; do
|
||||
container=`get-container-name`
|
||||
|
||||
while ! docker logs --since 1m $container 2>&1 | grep "Ready for tests" &> /dev/null; do
|
||||
sleep 1
|
||||
printf "."
|
||||
done
|
||||
|
||||
@@ -59,10 +59,15 @@ module OpenProject
|
||||
# https://guides.rubyonrails.org/configuring.html#versioned-default-values
|
||||
# for the default values associated with a particular version.
|
||||
#
|
||||
# Currently, defaults from Rails 4.2 are applied. Goal is to reach 7.0
|
||||
# defaults. Overridden defaults should be stored in specific initializers
|
||||
# files. See https://community.openproject.org/wp/45463 for details.
|
||||
# config.load_defaults 5.0
|
||||
# Goal is to reach 7.0 defaults. Overridden defaults should be stored in
|
||||
# specific initializers files. See
|
||||
# https://community.openproject.org/wp/45463 for details.
|
||||
config.load_defaults 5.0
|
||||
|
||||
# Do not require `belongs_to` associations to be present by default.
|
||||
# Rails 5.0+ default is true. Because of history, lots of tests fail when
|
||||
# set to true.
|
||||
config.active_record.belongs_to_required_by_default = false
|
||||
|
||||
# Use new connection handling API. For most applications this won't have any
|
||||
# effect. For applications using multiple databases, this new API provides
|
||||
|
||||
@@ -728,6 +728,18 @@ module Settings
|
||||
},
|
||||
writable: false
|
||||
},
|
||||
remote_storage_upload_host: {
|
||||
format: :string,
|
||||
default: nil,
|
||||
writable: false,
|
||||
description: 'Host the frontend uses to upload files to, which has to be added to the CSP.'
|
||||
},
|
||||
remote_storage_download_host: {
|
||||
format: :string,
|
||||
default: nil,
|
||||
writable: false,
|
||||
description: 'Host the frontend uses to download files, which has to be added to the CSP.'
|
||||
},
|
||||
report_incoming_email_errors: {
|
||||
description: 'Respond to incoming mails with error details',
|
||||
default: true
|
||||
|
||||
@@ -78,9 +78,9 @@ OpenProject::Application.configure do
|
||||
# Allow disabling HSTS redirect by using OPENPROJECT_HSTS=false
|
||||
config.force_ssl = OpenProject::Configuration.https?
|
||||
config.ssl_options = {
|
||||
hsts: OpenProject::Configuration.hsts_enabled?,
|
||||
# Disable redirect on the internal SYS API
|
||||
redirect: {
|
||||
hsts: OpenProject::Configuration.hsts_enabled?,
|
||||
exclude: ->(request) do
|
||||
# Disable redirects when hsts is disabled
|
||||
return true unless OpenProject::Configuration.hsts_enabled?
|
||||
|
||||
@@ -36,7 +36,10 @@ OpenProject::Application.configure do
|
||||
# test suite. You never need to work with it otherwise. Remember that
|
||||
# your test database is "scratch space" for the test suite and is wiped
|
||||
# and recreated between test runs. Don't rely on the data there!
|
||||
config.cache_classes = ENV['CI'].present?
|
||||
#
|
||||
# Spring requires to have the classes reloaded. On the CI or when Spring is
|
||||
# disabled, it does not need to happen.
|
||||
config.cache_classes = ENV['CI'].present? || ENV['DISABLE_SPRING'].present?
|
||||
|
||||
# Use eager load to mirror the production environment
|
||||
# on travis
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
OpenProject::Application.configure do |application|
|
||||
application.config.to_prepare do
|
||||
::Exports::Register.register do
|
||||
Exports::Register.register do
|
||||
list WorkPackage, WorkPackage::Exports::CSV
|
||||
list WorkPackage, ::WorkPackage::PDFExport::WorkPackageListToPdf
|
||||
list WorkPackage, WorkPackage::PDFExport::WorkPackageListToPdf
|
||||
|
||||
single WorkPackage, ::WorkPackage::PDFExport::WorkPackageToPdf
|
||||
single WorkPackage, WorkPackage::PDFExport::WorkPackageToPdf
|
||||
|
||||
formatter WorkPackage, WorkPackage::Exports::Formatters::Costs
|
||||
formatter WorkPackage, WorkPackage::Exports::Formatters::EstimatedHours
|
||||
@@ -13,6 +13,7 @@ OpenProject::Application.configure do |application|
|
||||
list Project, Projects::Exports::CSV
|
||||
formatter Project, Exports::Formatters::CustomField
|
||||
formatter Project, Projects::Exports::Formatters::Status
|
||||
formatter Project, Projects::Exports::Formatters::Description
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'mail/network/delivery_methods/smtp'
|
||||
|
||||
# Monkey patch mail 2.8.1 to make it possible to disable STARTTLS.
|
||||
# without having to change existing settings.
|
||||
# This brings in changes from https://github.com/mikel/mail/pull/1536,
|
||||
# which has not been released yet.
|
||||
module Mail
|
||||
class SMTP
|
||||
def initialize(values)
|
||||
self.settings = DEFAULTS
|
||||
settings[:enable_starttls_auto] = nil
|
||||
settings.merge!(values)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# `key` is said to be provided when `settings` has a non-nil value for `key`.
|
||||
def setting_provided?(key)
|
||||
!settings[key].nil?
|
||||
end
|
||||
|
||||
# Yields one of `:always`, `:auto` or `false` based on `enable_starttls` and `enable_starttls_auto` flags.
|
||||
# Yields `false` when `smtp_tls?`.
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def smtp_starttls
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
return false if smtp_tls?
|
||||
|
||||
if setting_provided?(:enable_starttls) && settings[:enable_starttls]
|
||||
# enable_starttls: provided and truthy
|
||||
case settings[:enable_starttls]
|
||||
when :auto then :auto
|
||||
when :always then :always
|
||||
# rubocop:disable Lint/DuplicateBranch
|
||||
else
|
||||
# rubocop:enable Lint/DuplicateBranch
|
||||
:always
|
||||
end
|
||||
elsif setting_provided?(:enable_starttls_auto)
|
||||
# enable_starttls: not provided or false
|
||||
settings[:enable_starttls_auto] ? :auto : false
|
||||
else
|
||||
# enable_starttls_auto: not provided
|
||||
# enable_starttls: when provided then false
|
||||
# use :auto when neither enable_starttls* provided
|
||||
setting_provided?(:enable_starttls) ? false : :auto
|
||||
end
|
||||
end
|
||||
|
||||
def smtp_tls?
|
||||
(setting_provided?(:tls) && settings[:tls]) || (setting_provided?(:ssl) && settings[:ssl])
|
||||
end
|
||||
|
||||
def start_smtp_session(&)
|
||||
build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password],
|
||||
settings[:authentication], &)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
def build_smtp_session
|
||||
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
if smtp_tls? && (settings[:enable_starttls] || settings[:enable_starttls_auto])
|
||||
# rubocop:disable Layout/LineLength
|
||||
raise ArgumentError,
|
||||
":enable_starttls and :tls are mutually exclusive. Set :tls if you're on an SMTPS connection. Set :enable_starttls if you're on an SMTP connection and using STARTTLS for secure TLS upgrade."
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
Net::SMTP.new(settings[:address], settings[:port]).tap do |smtp|
|
||||
if smtp_tls?
|
||||
smtp.disable_starttls
|
||||
smtp.enable_tls(ssl_context)
|
||||
else
|
||||
smtp.disable_tls
|
||||
|
||||
case smtp_starttls
|
||||
when :always
|
||||
smtp.enable_starttls(ssl_context)
|
||||
when :auto
|
||||
smtp.enable_starttls_auto(ssl_context)
|
||||
else
|
||||
smtp.disable_starttls
|
||||
end
|
||||
end
|
||||
|
||||
smtp.open_timeout = settings[:open_timeout] if settings[:open_timeout]
|
||||
smtp.read_timeout = settings[:read_timeout] if settings[:read_timeout]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -70,7 +70,7 @@ Redmine::MenuManager.map :quick_add_menu do |menu|
|
||||
{ controller: '/projects', action: :new, project_id: nil, parent_id: project&.id }
|
||||
},
|
||||
caption: ->(*) { Project.model_name.human },
|
||||
icon: "icon-add icon3",
|
||||
icon: "add",
|
||||
html: {
|
||||
aria: { label: I18n.t(:label_project_new) },
|
||||
title: I18n.t(:label_project_new)
|
||||
@@ -83,7 +83,7 @@ Redmine::MenuManager.map :quick_add_menu do |menu|
|
||||
menu.push :invite_user,
|
||||
nil,
|
||||
caption: :label_invite_user,
|
||||
icon: 'icon3 icon-user-plus',
|
||||
icon: 'user-plus',
|
||||
html: {
|
||||
'invite-user-modal-augment': 'invite-user-modal-augment'
|
||||
},
|
||||
@@ -128,35 +128,35 @@ Redmine::MenuManager.map :my_menu do |menu|
|
||||
menu.push :account,
|
||||
{ controller: '/my', action: 'account' },
|
||||
caption: :label_profile,
|
||||
icon: 'icon2 icon-user'
|
||||
icon: 'user'
|
||||
menu.push :settings,
|
||||
{ controller: '/my', action: 'settings' },
|
||||
caption: :label_setting_plural,
|
||||
icon: 'icon2 icon-settings2'
|
||||
icon: 'settings2'
|
||||
menu.push :password,
|
||||
{ controller: '/my', action: 'password' },
|
||||
caption: :button_change_password,
|
||||
if: Proc.new { User.current.change_password_allowed? },
|
||||
icon: 'icon2 icon-locked'
|
||||
icon: 'locked'
|
||||
menu.push :access_token,
|
||||
{ controller: '/my', action: 'access_token' },
|
||||
caption: I18n.t('my_account.access_tokens.access_tokens'),
|
||||
icon: 'icon2 icon-key'
|
||||
icon: 'key'
|
||||
menu.push :notifications,
|
||||
{ controller: '/my', action: 'notifications' },
|
||||
caption: I18n.t('js.notifications.settings.title'),
|
||||
icon: 'icon2 icon-bell'
|
||||
icon: 'bell'
|
||||
menu.push :reminders,
|
||||
{ controller: '/my', action: 'reminders' },
|
||||
caption: I18n.t('js.reminders.settings.title'),
|
||||
icon: 'icon2 icon-email-alert'
|
||||
icon: 'email-alert'
|
||||
|
||||
menu.push :delete_account, :delete_my_account_info_path,
|
||||
caption: I18n.t('account.delete'),
|
||||
param: :user_id,
|
||||
if: Proc.new { Setting.users_deletable_by_self? },
|
||||
last: :delete_account,
|
||||
icon: 'icon2 icon-delete'
|
||||
icon: 'delete'
|
||||
end
|
||||
|
||||
Redmine::MenuManager.map :admin_menu do |menu|
|
||||
@@ -164,26 +164,26 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/admin', action: :index },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_overview,
|
||||
icon: 'icon2 icon-home',
|
||||
icon: 'home',
|
||||
first: true
|
||||
|
||||
menu.push :users,
|
||||
{ controller: '/users' },
|
||||
if: Proc.new { !User.current.admin? && User.current.allowed_to_globally?(:manage_user) },
|
||||
caption: :label_user_plural,
|
||||
icon: 'icon2 icon-group'
|
||||
icon: 'group'
|
||||
|
||||
menu.push :placeholder_users,
|
||||
{ controller: '/placeholder_users' },
|
||||
if: Proc.new { !User.current.admin? && User.current.allowed_to_globally?(:manage_placeholder_user) },
|
||||
caption: :label_placeholder_user_plural,
|
||||
icon: 'icon2 icon-group'
|
||||
icon: 'group'
|
||||
|
||||
menu.push :users_and_permissions,
|
||||
{ controller: '/users' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_user_and_permission,
|
||||
icon: 'icon2 icon-group'
|
||||
icon: 'group'
|
||||
|
||||
menu.push :user_settings,
|
||||
{ controller: '/admin/settings/users_settings', action: :show },
|
||||
@@ -226,7 +226,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/admin/settings/work_packages_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_work_package_plural,
|
||||
icon: 'icon2 icon-view-timeline'
|
||||
icon: 'view-timeline'
|
||||
|
||||
menu.push :work_packages_setting,
|
||||
{ controller: '/admin/settings/work_packages_settings', action: :show },
|
||||
@@ -257,7 +257,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/custom_fields' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_custom_field_plural,
|
||||
icon: 'icon2 icon-custom-fields',
|
||||
icon: 'custom-fields',
|
||||
html: { class: 'custom_fields' }
|
||||
|
||||
menu.push :custom_actions,
|
||||
@@ -270,26 +270,26 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
menu.push :attribute_help_texts,
|
||||
{ controller: '/attribute_help_texts' },
|
||||
caption: :'attribute_help_texts.label_plural',
|
||||
icon: 'icon2 icon-help2',
|
||||
icon: 'help2',
|
||||
if: Proc.new { User.current.admin? },
|
||||
enterprise_feature: 'attribute_help_texts'
|
||||
|
||||
menu.push :enumerations,
|
||||
{ controller: '/enumerations' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
icon: 'icon2 icon-enumerations'
|
||||
icon: 'enumerations'
|
||||
|
||||
menu.push :working_days,
|
||||
{ controller: '/admin/settings/working_days_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_working_days,
|
||||
icon: 'icon2 icon-calendar'
|
||||
icon: 'calendar'
|
||||
|
||||
menu.push :settings,
|
||||
{ controller: '/admin/settings/general_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_system_settings,
|
||||
icon: 'icon2 icon-settings2'
|
||||
icon: 'settings2'
|
||||
|
||||
SettingsHelper.system_settings_tabs.each do |node|
|
||||
menu.push :"settings_#{node[:name]}",
|
||||
@@ -303,7 +303,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/admin/settings/aggregation_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :'menus.admin.mails_and_notifications',
|
||||
icon: 'icon2 icon-mail1'
|
||||
icon: 'mail1'
|
||||
|
||||
menu.push :notification_settings,
|
||||
{ controller: '/admin/settings/aggregation_settings', action: :show },
|
||||
@@ -327,7 +327,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/admin/settings/api_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :'menus.admin.api_and_webhooks',
|
||||
icon: 'icon2 icon-relations'
|
||||
icon: 'relations'
|
||||
|
||||
menu.push :api,
|
||||
{ controller: '/admin/settings/api_settings', action: :show },
|
||||
@@ -339,7 +339,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/admin/settings/authentication_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_authentication,
|
||||
icon: 'icon2 icon-two-factor-authentication'
|
||||
icon: 'two-factor-authentication'
|
||||
|
||||
menu.push :authentication_settings,
|
||||
{ controller: '/admin/settings/authentication_settings', action: :show },
|
||||
@@ -365,52 +365,52 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/announcements', action: 'edit' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_announcement,
|
||||
icon: 'icon2 icon-news'
|
||||
icon: 'news'
|
||||
|
||||
menu.push :plugins,
|
||||
{ controller: '/admin', action: 'plugins' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
last: true,
|
||||
icon: 'icon2 icon-plugins'
|
||||
icon: 'plugins'
|
||||
|
||||
menu.push :backups,
|
||||
{ controller: '/admin/backups', action: 'show' },
|
||||
if: Proc.new { OpenProject::Configuration.backup_enabled? && User.current.allowed_to_globally?(Backup.permission) },
|
||||
caption: :label_backup,
|
||||
last: true,
|
||||
icon: 'icon2 icon-save'
|
||||
icon: 'save'
|
||||
|
||||
menu.push :info,
|
||||
{ controller: '/admin', action: 'info' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_information_plural,
|
||||
last: true,
|
||||
icon: 'icon2 icon-info1'
|
||||
icon: 'info1'
|
||||
|
||||
menu.push :custom_style,
|
||||
{ controller: '/custom_styles', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_custom_style,
|
||||
icon: 'icon2 icon-design',
|
||||
icon: 'design',
|
||||
enterprise_feature: 'define_custom_style'
|
||||
|
||||
menu.push :colors,
|
||||
{ controller: '/colors', action: 'index' },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :'timelines.admin_menu.colors',
|
||||
icon: 'icon2 icon-status'
|
||||
icon: 'status'
|
||||
|
||||
menu.push :enterprise,
|
||||
{ controller: '/enterprises', action: :show },
|
||||
caption: :label_enterprise_edition,
|
||||
icon: 'icon2 icon-enterprise-addons',
|
||||
icon: 'enterprise-addons',
|
||||
if: proc { User.current.admin? && OpenProject::Configuration.ee_manager_visible? }
|
||||
|
||||
menu.push :admin_costs,
|
||||
{ controller: '/admin/settings', action: 'show_plugin', id: :costs },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :project_module_costs,
|
||||
icon: 'icon2 icon-budget'
|
||||
icon: 'budget'
|
||||
|
||||
menu.push :costs_setting,
|
||||
{ controller: '/admin/settings', action: 'show_plugin', id: :costs },
|
||||
@@ -422,7 +422,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
{ controller: '/backlogs_settings', action: :show },
|
||||
if: Proc.new { User.current.admin? },
|
||||
caption: :label_backlogs,
|
||||
icon: 'icon2 icon-backlogs'
|
||||
icon: 'backlogs'
|
||||
|
||||
menu.push :backlogs_settings,
|
||||
{ controller: '/backlogs_settings', action: :show },
|
||||
@@ -435,17 +435,17 @@ Redmine::MenuManager.map :project_menu do |menu|
|
||||
menu.push :activity,
|
||||
{ controller: '/activities', action: 'index' },
|
||||
if: Proc.new { |p| p.module_enabled?('activity') },
|
||||
icon: 'icon2 icon-checkmark'
|
||||
icon: 'checkmark'
|
||||
|
||||
menu.push :roadmap,
|
||||
{ controller: '/versions', action: 'index' },
|
||||
if: Proc.new { |p| p.shared_versions.any? },
|
||||
icon: 'icon2 icon-roadmap'
|
||||
icon: 'roadmap'
|
||||
|
||||
menu.push :work_packages,
|
||||
{ controller: '/work_packages', action: 'index' },
|
||||
caption: :label_work_package_plural,
|
||||
icon: 'icon2 icon-view-timeline',
|
||||
icon: 'view-timeline',
|
||||
html: {
|
||||
id: 'main-menu-work-packages',
|
||||
'wp-query-menu': 'wp-query-menu'
|
||||
@@ -461,17 +461,17 @@ Redmine::MenuManager.map :project_menu do |menu|
|
||||
menu.push :news,
|
||||
{ controller: '/news', action: 'index' },
|
||||
caption: :label_news_plural,
|
||||
icon: 'icon2 icon-news'
|
||||
icon: 'news'
|
||||
|
||||
menu.push :forums,
|
||||
{ controller: '/forums', action: 'index', id: nil },
|
||||
caption: :label_forum_plural,
|
||||
icon: 'icon2 icon-ticket-note'
|
||||
icon: 'ticket-note'
|
||||
|
||||
menu.push :repository,
|
||||
{ controller: '/repositories', action: :show },
|
||||
if: Proc.new { |p| p.repository && !p.repository.new_record? },
|
||||
icon: 'icon2 icon-folder-open'
|
||||
icon: 'folder-open'
|
||||
|
||||
# Wiki menu items are added by WikiMenuItemHelper
|
||||
|
||||
@@ -479,13 +479,13 @@ Redmine::MenuManager.map :project_menu do |menu|
|
||||
{ controller: '/members', action: 'index' },
|
||||
caption: :label_member_plural,
|
||||
before: :settings,
|
||||
icon: 'icon2 icon-group'
|
||||
icon: 'group'
|
||||
|
||||
menu.push :settings,
|
||||
{ controller: '/projects/settings/general', action: :show },
|
||||
caption: :label_project_settings,
|
||||
last: true,
|
||||
icon: 'icon2 icon-settings2',
|
||||
icon: 'settings2',
|
||||
allow_deeplink: true
|
||||
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file contains migration options to ease your Rails 5.0 upgrade.
|
||||
#
|
||||
# Uncomment each configuration one by one to switch to the new default.
|
||||
# Once your application is ready to run with all new defaults, you can remove
|
||||
# this file and set the `config.load_defaults` to `5.0`.
|
||||
#
|
||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
||||
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
|
||||
|
||||
# https://guides.rubyonrails.org/configuring.html#config-action-controller-per-form-csrf-tokens
|
||||
# Enable per-form CSRF tokens. Previous versions had false. Rails 5.0+ default
|
||||
# is true.
|
||||
# Rails.application.config.action_controller.per_form_csrf_tokens = true
|
||||
|
||||
# https://guides.rubyonrails.org/configuring.html#config-action-controller-forgery-protection-origin-check
|
||||
# Enable origin-checking CSRF mitigation. Previous versions had false. Rails
|
||||
# 5.0+ default is true.
|
||||
# Rails.application.config.action_controller.forgery_protection_origin_check = true
|
||||
|
||||
# https://guides.rubyonrails.org/configuring.html#activesupport-to-time-preserves-timezone
|
||||
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
|
||||
# Previous versions had false. Rails 5.0+ default is true.
|
||||
# ActiveSupport.to_time_preserves_timezone = true
|
||||
|
||||
# https://guides.rubyonrails.org/configuring.html#config-active-record-belongs-to-required-by-default
|
||||
# Require `belongs_to` associations by default. Previous versions had false.
|
||||
# Rails 5.0+ default is true.
|
||||
# Rails.application.config.active_record.belongs_to_required_by_default = true
|
||||
|
||||
# https://guides.rubyonrails.org/configuring.html#config-ssl-options
|
||||
# Configure SSL options to enable HSTS with subdomains. Previous versions had
|
||||
# false. Rails 5.0+ default is `subdomains: true` to apply HSTS to subdomains.
|
||||
# Please note that OpenProject sets it through secure_headers gem: look in
|
||||
# config/initializers/secure_headers.rb:9
|
||||
# Rails.application.config.ssl_options = { hsts: { subdomains: true } }
|
||||
@@ -5,8 +5,10 @@ Rails.application.config.after_initialize do
|
||||
secure: true,
|
||||
httponly: true
|
||||
}
|
||||
# Add "; preload" and submit the site to hstspreload.org for best protection.
|
||||
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains"
|
||||
|
||||
# Let Rails ActionDispatch::SSL middleware handle the Strict-Transport-Security header
|
||||
config.hsts = SecureHeaders::OPT_OUT
|
||||
|
||||
config.x_frame_options = "SAMEORIGIN"
|
||||
config.x_content_type_options = "nosniff"
|
||||
config.x_xss_protection = "1; mode=block"
|
||||
|
||||
@@ -80,7 +80,7 @@ af:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ af:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ af:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ ar:
|
||||
buttons:
|
||||
upgrade: "الترقية الآن"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -744,6 +744,9 @@ ar:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2723,8 +2726,6 @@ ar:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ az:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ az:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ az:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ be:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -742,6 +742,9 @@ be:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2692,8 +2695,6 @@ be:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ bg:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ bg:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ bg:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ ca:
|
||||
buttons:
|
||||
upgrade: "Actualitza ara"
|
||||
contact: "Contacta amb nosaltres per una demostració"
|
||||
enterprise_info_html: "és un add-on de l'edició Enterprise <span class='spot-icon spot-icon_enterprise-badge'></span>."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Si us plau, actualitza a una versió de pagament per tal d'activar i començar a utilitzar aquesta funcionalitat en el teu equip."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -229,7 +229,7 @@ ca:
|
||||
failed_text: "La petició per a eliminar el projecte %{name} ha fallat. El projecte s'ha arxivat."
|
||||
completed: "S'ha completat l'eliminació del projecte %{name}"
|
||||
completed_text: "La petició d'eliminació del projecte '%{name}' s'ha completat."
|
||||
completed_text_children: "Additionally, the following subprojects have been deleted:"
|
||||
completed_text_children: "Addicionalment, s'han eliminat els següents subprojectes:"
|
||||
index:
|
||||
open_as_gantt: 'Obre com a diagrama de Gantt'
|
||||
open_as_gantt_title: "Utilitza aquest botó per a generar un diagrama de Gantt que filtra paquets de treball pels projectes visibles en aquesta pàgina."
|
||||
@@ -736,6 +736,9 @@ ca:
|
||||
sort_criteria:
|
||||
invalid: "No es pot ordenar per columna: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "és mútuament exclusiu amb el grup per \"%{group_by}\". No pots activar els dos."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2643,8 +2646,6 @@ ca:
|
||||
Mentre utilitzes camps personalitzats: Tingues en compte que els camps personalitzats s'han d'activar per projecte també.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Els camps personalitzats han d'activar-se per classe de paquet de treball i per projecte.
|
||||
text_wp_custom_field_html: >
|
||||
L'edició Enterprise afegirà add-ons extra pels camps personalitzats de paquets de treball: <br> <ul> <li><b>Permet múltiple selecció dels camps personalitzats pels estils Llista o Usuari</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
L'edició Enterprise afegirà add-ons extra pels estats de paquets de treball: <br> <ul> <li><b>Permet marcar paquets de treball com a només lectura per a estats específics</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
@@ -3007,7 +3008,7 @@ ca:
|
||||
code_409: "No s'ha pogut actualitzar el recurs a causa de modificacions en conflicte."
|
||||
code_429: "Masses demandes. Si us plau, prova-ho de nou més tard."
|
||||
code_500: "S'ha produït un error intern."
|
||||
code_500_outbound_request_failure: "An outbound request to another resource has failed with status code %{status_code}."
|
||||
code_500_outbound_request_failure: "Hi ha un error en una petició sortint a un altre recurs amb el codi d'estat %{status_code}."
|
||||
not_found:
|
||||
work_package: "No s'ha pogut trobar o s'ha eliminat el paquet de treball que estàs buscant."
|
||||
expected:
|
||||
|
||||
@@ -80,7 +80,7 @@ ckb-IR:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ ckb-IR:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ ckb-IR:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ cs:
|
||||
buttons:
|
||||
upgrade: "Upgradovat nyní"
|
||||
contact: "Kontaktujte nás pro demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Přejděte na placenou verzi a začněte ji používat ve vašem týmu."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -742,6 +742,9 @@ cs:
|
||||
sort_criteria:
|
||||
invalid: "Nelze řadit podle sloupce: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "je vzájemně exkluzivní se skupinou od '%{group_by}'. Nelze aktivovat obojí."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2691,8 +2694,6 @@ cs:
|
||||
Při používání vlastních polí: Mějte na paměti, že vlastní pole musí být aktivována také pro každý projekt.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Vlastní pole je třeba aktivovat podle typu pracovního balíčku a podle projektu.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ da:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -738,6 +738,9 @@ da:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2650,8 +2653,6 @@ da:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ de:
|
||||
buttons:
|
||||
upgrade: "Jetzt Upgrade durchführen"
|
||||
contact: "Kontaktieren Sie uns für eine Demo"
|
||||
enterprise_info_html: "ist ein Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> Add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Bitte steigen Sie auf einen kostenpflichtigen Plan um, um diese Funktion zu aktivieren und in Ihrem Team zu verwenden."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -735,6 +735,9 @@ de:
|
||||
sort_criteria:
|
||||
invalid: "Kann nicht nach Spalte sortieren: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Zeitstempel enthalten ungültige Werte: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "schließt sich gegenseitig mit der Gruppierung nach \"%{group_by}\" aus. Beide können nicht zeitgleich aktiv sein."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2649,8 +2652,6 @@ de:
|
||||
Wenn Sie benutzerdefinierte Felder verwenden: Bitte beachten, dass diese auch pro Projekt aktiviert werden müssen.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Benutzerdefinierte Felder müssen jeweils in Arbeitspaket-Typ und Projekt aktiviert werden.
|
||||
text_wp_custom_field_html: >
|
||||
Die Enterprise Edition fügt diese zusätzlichen Add-ons für benutzerdefinierte Felder von Arbeitspaketen hinzu: <br> <ul> <li><b>Erlaubt Mehrfachauswahl für benutzerdefinierte Felder der Typenliste oder Benutzer</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
Die Enterprise Edition fügt diese zusätzlichen Add-ons für die Statusfelder der Arbeitspakete hinzu: <br> <ul> <li><b>Erlaubt die Markierung von Arbeitspaketen als read-only für bestimmte Status</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
@@ -3012,7 +3013,7 @@ de:
|
||||
code_409: "Die Ressource konnte wegen parallelen Zugriffs nicht aktualisiert werden."
|
||||
code_429: "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
||||
code_500: "Ein interner Fehler ist aufgetreten."
|
||||
code_500_outbound_request_failure: "An outbound request to another resource has failed with status code %{status_code}."
|
||||
code_500_outbound_request_failure: "Eine ausgehende Anfrage an eine andere Ressource ist mit dem Statuscode %{status_code} fehlgeschlagen."
|
||||
not_found:
|
||||
work_package: "Das von Ihnen gesuchte Arbeitspaket konnte nicht gefunden werden oder wurde gelöscht."
|
||||
expected:
|
||||
|
||||
@@ -80,7 +80,7 @@ el:
|
||||
buttons:
|
||||
upgrade: "Αναβαθμίστε τώρα"
|
||||
contact: "Επικοινωνήστε μαζί μας για μια επίδειξη"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -736,6 +736,9 @@ el:
|
||||
sort_criteria:
|
||||
invalid: "Δεν είναι δυνατή η ταξινόμηση κατά στήλη: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "είναι αμοιβαία αποκλειστικό με την ομαδοποίηση κατά '%{group_by}'. Δεν μπορείτε να ενεργοποιήσετε και τα δύο."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2649,8 +2652,6 @@ el:
|
||||
Όταν χρησιμοποιείτε προσαρμοσμένα πεδία: Θυμηθείτε ότι τα προσαρμοσμένα πεδία πρέπει να ενεργοποιημένα και ανά έργο.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Τα προσαρμοσμένα πεδία πρέπει να είναι ενεργοποιημένα ανά τύπο πακέτου εργασίας και ανά έργο.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ eo:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ eo:
|
||||
sort_criteria:
|
||||
invalid: "Ne ordigebla laŭ kolono: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ eo:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ es:
|
||||
buttons:
|
||||
upgrade: "Actualizar ahora"
|
||||
contact: "Contáctenos para una demostración"
|
||||
enterprise_info_html: "es un add-on de la edición Enterprise <span class='spot-icon spot-icon_enterprise-badge'></span>."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Actualice a un plan de pago para activarlo y empezar a usarlo en su equipo."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -229,7 +229,7 @@ es:
|
||||
failed_text: "La petición de borrado del proyecto %{name} ha fallado. El proyecto ha sido archivado."
|
||||
completed: "Borrado del proyecto %{name} completado"
|
||||
completed_text: "La petición para borrar el proyecto '%{name}' se ha completado."
|
||||
completed_text_children: "Additionally, the following subprojects have been deleted:"
|
||||
completed_text_children: "Adicionalmente, los siguientes subproyectos han sido eliminados:"
|
||||
index:
|
||||
open_as_gantt: 'Abrir como diagrama de Gantt'
|
||||
open_as_gantt_title: "Use este botón para generar un diagrama de Gantt donde se filtren los paquetes de trabajo de los proyectos visibles en esta página."
|
||||
@@ -737,6 +737,9 @@ es:
|
||||
sort_criteria:
|
||||
invalid: "No se puede ordenar por la columna: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "es mutuamente exclusivo con el grupo '%{group_by}'. No puede activar ambos."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2650,8 +2653,6 @@ es:
|
||||
Si utiliza campos personalizados: tenga en cuenta que los campos personalizados deben activarse también en cada proyecto por separado.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Los campos personalizados se deben activar en cada tipo de paquete de trabajo y en cada proyecto por separado.
|
||||
text_wp_custom_field_html: >
|
||||
La edición Enterprise añadirá estos add-ons extra para los campos personalizados de los paquetes de trabajo: <br> <ul> <li><b>Permitir la selección múltiple de campos personalizados de tipo Lista o Usuario</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
La edición Enterprise añadirá estos add-ons extras para los campos de estado de los paquetes de trabajo: <br> <ul> <li><b>Permitir marcar como solo lectura los paquetes de trabajo en estados específicos</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
@@ -3013,7 +3014,7 @@ es:
|
||||
code_409: "No se pudo actualizar el recurso debido a un conflicto de modificaciones."
|
||||
code_429: "Demasiadas solicitudes. Vuelva a intentarlo más tarde."
|
||||
code_500: "Ha ocurrido un error interno."
|
||||
code_500_outbound_request_failure: "An outbound request to another resource has failed with status code %{status_code}."
|
||||
code_500_outbound_request_failure: "Una solicitud de salida a otro recurso ha fallado con el código de estado %{status_code}."
|
||||
not_found:
|
||||
work_package: "No se encuentra el paquete de trabajo que busca, o bien se ha eliminado."
|
||||
expected:
|
||||
|
||||
@@ -80,7 +80,7 @@ et:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ et:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ et:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ eu:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ eu:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ eu:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ fa:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ fa:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ fa:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ fi:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ fi:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ fi:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ fil:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ fil:
|
||||
sort_criteria:
|
||||
invalid: "Hindi masunod sa pamamagitan ng hanay: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "ay mutually wxclusive sa grupo ng '%{group_by}'. Hindi mo maaring i-aktibi ang dalawa."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2652,8 +2655,6 @@ fil:
|
||||
Kung gagamit ng mga custom na patlang. Laging isaisip na ang mga custom na patlang ay kailangan dapat aktibo bawat proyekto, din.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Ang mga custom na patlang ay kailangan dapat aktibo sa bawat uri ng work package at bawat proyekto.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ fr:
|
||||
buttons:
|
||||
upgrade: "Passer au plan supérieur"
|
||||
contact: "Contactez-nous pour une démo"
|
||||
enterprise_info_html: "est un <span class='spot-icon spot-icon_enterprise-addons'></span> add-on Entreprise."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Veuillez passer à un plan payant pour l'activer et commencer à l'utiliser dans votre équipe."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -740,6 +740,9 @@ fr:
|
||||
sort_criteria:
|
||||
invalid: "Impossible de trier par colonne : %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "est mutuellement exclusif avec grouper par '%{group_by}'. Vous ne pouvez pas activer les deux."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2654,8 +2657,6 @@ fr:
|
||||
Lorsque vous utilisez des champs personnalisés : n’oubliez pas que les champs personnalisés doivent être activés par projet également.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Les champs personnalisés doivent être activés par type de lot de travaux et par projet.
|
||||
text_wp_custom_field_html: >
|
||||
L'édition Entreprise ajoutera ces add-ons supplémentaires pour les champs personnalisés des lots de travaux : <br> <ul> <li><b>Autoriser la sélection multiple pour les champs personnalisés de type Liste ou Utilisateur</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
L'édition Entreprise ajoutera ces add-ons supplémentaires pour les champs de statut des lots de travaux : <br> <ul> <li><b>Permet de marquer les lots de travaux en lecture seule pour des statuts spécifiques</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ he:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -742,6 +742,9 @@ he:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2692,8 +2695,6 @@ he:
|
||||
When using custom fields: Keep in mind that custom fields need to be activated per project, too.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
Custom fields need to be activated per work package type and per project.
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
@@ -80,7 +80,7 @@ hi:
|
||||
buttons:
|
||||
upgrade: "Upgrade now"
|
||||
contact: "Contact us for a demo"
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_enterprise-addons'></span> add-on."
|
||||
enterprise_info_html: "is an Enterprise <span class='spot-icon spot-icon_inline spot-icon_enterprise-addons'></span> add-on."
|
||||
upgrade_info: "Please upgrade to a paid plan to activate and start using it in your team."
|
||||
journal_aggregation:
|
||||
explanation:
|
||||
@@ -738,6 +738,9 @@ hi:
|
||||
sort_criteria:
|
||||
invalid: "Can't sort by column: %{value}"
|
||||
format: "%{message}"
|
||||
timestamps:
|
||||
invalid: "Timestamps contain invalid values: %{values}"
|
||||
format: "%{message}"
|
||||
group_by_hierarchies_exclusive: "is mutually exclusive with group by '%{group_by}'. You cannot activate both."
|
||||
filters:
|
||||
custom_fields:
|
||||
@@ -2652,8 +2655,6 @@ hi:
|
||||
कस्टम फ़ील्ड का उपयोग करते समय: ध्यान रखें कि कस्टम फ़ील्ड्स को प्रति प्रोजेक्ट सक्रिय करने की आवश्यकता भी है.
|
||||
text_custom_field_hint_activate_per_project_and_type: >
|
||||
कस्टम फ़ील्ड प्रति कार्य पैकेज प्रकार और प्रति प्रोजेक्ट सक्रिय करने की आवश्यकता है ।
|
||||
text_wp_custom_field_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' custom fields: <br> <ul> <li><b>Allow multi-select for custom fields of type List or User</b></li> </ul>
|
||||
text_wp_status_read_only_html: >
|
||||
The Enterprise edition will add these additional add-ons for work packages' statuses fields: <br> <ul> <li><b>Allow to mark work packages to read-only for specific statuses</b></li> </ul>
|
||||
text_project_custom_field_html: >
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user