Merge branch 'dev' into fix/merge_wiki_content_into_page

This commit is contained in:
ulferts
2023-04-28 09:27:18 +02:00
1532 changed files with 35512 additions and 59544 deletions
+2 -2
View File
@@ -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
+1
View File
@@ -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: |
+10 -6
View File
@@ -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
View File
@@ -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)
+3 -3
View File
@@ -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
+10
View File
@@ -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
+16 -1
View File
@@ -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
+1 -7
View File
@@ -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
+1 -3
View File
@@ -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
+7 -3
View File
@@ -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
##
+3 -2
View File
@@ -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
##
+9
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
+1 -1
View File
@@ -57,7 +57,7 @@ class NotificationSetting < ApplicationRecord
]
end
belongs_to :project
belongs_to :project, optional: true
belongs_to :user
include Scopes::Scoped
+5
View File
@@ -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
#
@@ -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
+1 -1
View File
@@ -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'
+5 -3
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+16 -26
View File
@@ -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
+77 -57
View File
@@ -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
+21 -12
View File
@@ -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
+2 -4
View File
@@ -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
+2 -33
View File
@@ -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
+106
View File
@@ -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
+93 -128
View File
@@ -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
+7 -5
View File
@@ -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?
+7 -6
View File
@@ -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
+7 -10
View File
@@ -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
+37 -43
View File
@@ -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
-1
View File
@@ -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
+17 -17
View File
@@ -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
+3 -3
View File
@@ -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
-1
View File
@@ -31,7 +31,6 @@ class DevelopmentDataSeeder < CompositeSeeder
DevelopmentData::UsersSeeder,
DevelopmentData::CustomFieldsSeeder,
DevelopmentData::ProjectsSeeder
# DevelopmentData::WorkPackageSeeder
]
end
+65 -38
View File
@@ -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
+71
View File
@@ -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
View File
@@ -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(&)
+705
View File
@@ -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: '![Teaser](##attachment:"demo_project_teaser.png")'
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*: &rightarrow; Go to [Members]({{opSetting:base_url}}/projects/demo-project/members) in the project navigation.
2. *View the work in your project*: &rightarrow; Go to [Work packages]({{opSetting:base_url}}/projects/demo-project/work_packages) in the project navigation.
3. *Create a new work package*: &rightarrow; Go to [Work packages &rightarrow; Create]({{opSetting:base_url}}/projects/demo-project/work_packages/new).
4. *Create and update a project plan*: &rightarrow; 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*: &rightarrow; Go to [Project settings &rightarrow; Modules]({{opSetting:base_url}}/projects/demo-project/settings/modules).
6. *Complete your tasks in the project*: &rightarrow; Go to [Work packages &rightarrow; 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: '![Teaser](##attachment:"scrum_project_teaser.png")'
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*: &rightarrow; Go to [Members]({{opSetting:base_url}}/projects/your-scrum-project/members) in the project navigation.
2. *View your Product backlog and Sprint backlogs*: &rightarrow; Go to [Backlogs]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) in the project navigation.
3. *View your Task board*: &rightarrow; Go to [Backlogs]({{opSetting:base_url}}/projects/your-scrum-project/backlogs) &rightarrow; Click on right arrow on Sprint &rightarrow; Select [Task Board](##sprint:"Sprint 1").
4. *Create a new work package*: &rightarrow; Go to [Work packages &rightarrow; Create]({{opSetting:base_url}}/projects/your-scrum-project/work_packages/new).
5. *Create and update a project plan*: &rightarrow; Go to [Project plan](##query:"Project plan") in the project navigation.
6. *Create a Sprint wiki*: &rightarrow; 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*: &rightarrow; Go to [Project settings &rightarrow; 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.
@@ -25,7 +25,7 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
module StandardSeeder
module Standard
module BasicData
class ActivitySeeder < ::BasicData::ActivitySeeder
def data
@@ -25,7 +25,7 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
module StandardSeeder
module Standard
module BasicData
class PrioritySeeder < ::BasicData::PrioritySeeder
def data
@@ -25,7 +25,7 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
module StandardSeeder
module Standard
module BasicData
class StatusSeeder < ::BasicData::StatusSeeder
def data
@@ -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
@@ -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
@@ -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
+2 -2
View File
@@ -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)
+5 -1
View File
@@ -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?
+10 -2
View File
@@ -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>
+1 -1
View File
@@ -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 %>
+1 -1
View File
@@ -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>
+3 -21
View File
@@ -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>
-13
View File
@@ -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">
+1 -1
View File
@@ -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
View File
@@ -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
+9 -4
View File
@@ -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
+12
View File
@@ -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
+1 -1
View File
@@ -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?
+4 -1
View File
@@ -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
+4 -3
View File
@@ -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
+39 -39
View File
@@ -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 } }
+4 -2
View File
@@ -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"
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+6 -5
View File
@@ -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:
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+5 -4
View File
@@ -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:
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+6 -5
View File
@@ -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:
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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 : noubliez 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: >
+4 -3
View File
@@ -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: >
+4 -3
View File
@@ -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