Skip to content

Commit

Permalink
Merge branch 'release/10.6' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
ulferts committed Jul 8, 2020
2 parents b2f828d + f147c09 commit 356ed62
Show file tree
Hide file tree
Showing 41 changed files with 735 additions and 58 deletions.
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ gem 'sprockets', '~> 3.7.0'
# also, better than thin since we can control worker concurrency.
gem 'unicorn'

gem 'puma', '~> 4.3.1' # used for development and optionally for production
gem 'puma', '~> 4.3.5' # used for development and optionally for production

gem 'nokogiri', '~> 1.10.8'

Expand All @@ -175,7 +175,7 @@ gem 'aws-sdk-core', '~> 3.91.0'
# File upload via fog + screenshots on travis
gem 'aws-sdk-s3', '~> 1.61.0'

gem 'openproject-token', '~> 2.0'
gem 'openproject-token', '~> 2.1.1'

gem 'plaintext', '~> 0.3.2'

Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ GEM
validate_email
validate_url
webfinger (>= 1.0.1)
openproject-token (2.0)
openproject-token (2.1.2)
activemodel
parallel (1.19.1)
parallel_tests (2.32.0)
Expand Down Expand Up @@ -1060,7 +1060,7 @@ DEPENDENCIES
openproject-pdf_export!
openproject-recaptcha!
openproject-reporting!
openproject-token (~> 2.0)
openproject-token (~> 2.1.1)
openproject-translations!
openproject-two_factor_authentication!
openproject-webhooks!
Expand All @@ -1078,7 +1078,7 @@ DEPENDENCIES
pry-rescue (~> 1.5.0)
pry-stack_explorer (~> 0.4.9.2)
puffing-billy (~> 2.3.1)
puma (~> 4.3.1)
puma (~> 4.3.5)
rack-attack (~> 6.2.2)
rack-mini-profiler
rack-protection (~> 2.0.8)
Expand Down
32 changes: 17 additions & 15 deletions app/models/custom_field/order_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,16 @@ def coalesce_select_custom_value_as_string
# COALESCE is here to make sure that blank and NULL values are sorted equally
[
<<-SQL
COALESCE(#{select_custom_value_as_string}, '')
COALESCE(#{select_custom_value_as_string}, '')
SQL
]
end

def select_custom_value_as_string
<<-SQL
(SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort
WHERE cv_sort.customized_type='#{self.class.customized_class.name}'
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id} LIMIT 1)
WHERE #{cv_sort_only_custom_field_condition_sql}
LIMIT 1)
SQL
end

Expand All @@ -109,8 +108,7 @@ def select_custom_option_position
(SELECT co_sort.position FROM #{CustomOption.table_name} co_sort
LEFT JOIN #{CustomValue.table_name} cv_sort
ON co_sort.id = CAST(cv_sort.value AS decimal(60,3))
WHERE cv_sort.custom_field_id=#{id}
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
WHERE #{cv_sort_only_custom_field_condition_sql}
LIMIT 1
)
SQL
Expand All @@ -119,9 +117,7 @@ def select_custom_option_position
def select_custom_values_as_group
<<-SQL
COALESCE((SELECT string_agg(cv_sort.value, '.') FROM #{CustomValue.table_name} cv_sort
WHERE cv_sort.customized_type='#{self.class.customized_class.name}'
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id}
WHERE #{cv_sort_only_custom_field_condition_sql}
AND cv_sort.value IS NOT NULL), '')
SQL
end
Expand All @@ -131,18 +127,14 @@ def select_custom_values_joined_options_as_group
COALESCE((SELECT string_agg(co_sort.value, '.' ORDER BY co_sort.position ASC) FROM #{CustomOption.table_name} co_sort
LEFT JOIN #{CustomValue.table_name} cv_sort
ON cv_sort.value IS NOT NULL AND co_sort.id = cv_sort.value::numeric
WHERE cv_sort.customized_type='#{self.class.customized_class.name}'
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id}), '')
WHERE #{cv_sort_only_custom_field_condition_sql}), '')
SQL
end

def select_custom_value_as_decimal
<<-SQL
(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort
WHERE cv_sort.customized_type='#{self.class.customized_class.name}'
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id}
WHERE #{cv_sort_only_custom_field_condition_sql}
AND cv_sort.value <> ''
AND cv_sort.value IS NOT NULL
LIMIT 1)
Expand All @@ -164,4 +156,14 @@ def order_by_version_sql(column)
LIMIT 1)
SQL
end

private

def cv_sort_only_custom_field_condition_sql
<<-SQL
cv_sort.customized_type='#{self.class.customized_class.name}'
AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id
AND cv_sort.custom_field_id=#{id}
SQL
end
end
7 changes: 7 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ class Project < ApplicationRecord
validates :name,
presence: true,
length: { maximum: 255 }

before_validation :remove_white_spaces_from_project_name

# TODO: we temporarily disable this validation because it leads to failed tests
# it implicitly assumes a db:seed-created standard type to be present and currently
# neither development nor deployment setups are prepared for this
Expand Down Expand Up @@ -573,4 +576,8 @@ def shared_versions_base_scope
.includes(:project)
.references(:projects)
end

def remove_white_spaces_from_project_name
self.name = name.squish unless self.name.nil?
end
end
10 changes: 6 additions & 4 deletions app/services/groups/add_users_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def add_to_user_and_projects_cte
SELECT members.project_id AS project_id,
members.user_id AS user_id,
members.id AS member_id,
member_roles.role_id AS role_id
member_roles.role_id AS role_id,
member_roles.id AS member_role_id
FROM #{MemberRole.table_name} member_roles
JOIN #{Member.table_name} members
ON members.id = member_roles.member_id AND members.user_id = :group_id
Expand All @@ -103,12 +104,13 @@ def add_to_user_and_projects_cte
-- even if they already exist as members (e.g., added individually) to ensure we add all roles
-- to mark that we reset the created_at date since replacing the member
ON CONFLICT(project_id, user_id) DO UPDATE SET created_on = CURRENT_TIMESTAMP
RETURNING id, user_id
RETURNING id, user_id, project_id
)
-- copy the member roles of the group
INSERT INTO #{MemberRole.table_name} (member_id, role_id, inherited_from)
SELECT new_members.id, group_roles.role_id, group_roles.member_id
FROM group_roles, new_members
SELECT new_members.id, group_roles.role_id, group_roles.member_role_id
FROM group_roles
JOIN new_members ON group_roles.project_id = new_members.project_id
-- Ignore if the role was already inserted by us
ON CONFLICT DO NOTHING
SQL
Expand Down
26 changes: 19 additions & 7 deletions app/services/work_packages/set_schedule_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,13 @@ def reschedule_ancestor(scheduled, dependency)
# ancestors limits moving it. Then it is moved to the earliest date possible. This limitation is propagated transtitively
# to all following work packages.
def reschedule_by_follows(scheduled, dependency)
delta = if dependency.follows_moved.first
date_rescheduling_delta(dependency.follows_moved.first.to)
else
0
end

delta = follows_delta(dependency)
min_start_date = dependency.max_date_of_followed

if delta.zero? && min_start_date
reschedule_to_date(scheduled, min_start_date)
elsif !scheduled.start_date
schedule_on_missing_dates(scheduled, min_start_date)
elsif !delta.zero?
reschedule_by_delta(scheduled, delta, min_start_date)
end
Expand All @@ -149,9 +146,24 @@ def reschedule_to_date(scheduled, date)
end

def reschedule_by_delta(scheduled, delta, min_start_date)
required_delta = [min_start_date - scheduled.start_date, [delta, 0].min].max
required_delta = [min_start_date - (scheduled.start_date || min_start_date), [delta, 0].min].max

scheduled.start_date += required_delta
scheduled.due_date += required_delta if scheduled.due_date
end

# If the start_date of scheduled is nil at this point something
# went wrong before. So we fix it now by setting the date.
def schedule_on_missing_dates(scheduled, min_start_date)
scheduled.start_date = min_start_date
scheduled.due_date = scheduled.start_date + 1 if scheduled.due_date && scheduled.due_date < scheduled.start_date
end

def follows_delta(dependency)
if dependency.follows_moved.first
date_rescheduling_delta(dependency.follows_moved.first.to)
else
0
end
end
end
53 changes: 52 additions & 1 deletion config/initializers/health_checks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,53 @@ def check
end
end

class PumaCheck < OkComputer::Check
attr_reader :threshold

def initialize(backlog_threshold)
@threshold = backlog_threshold.to_i
end

def check
stats = self.stats

return mark_message "N/A as Puma is not used." if stats.nil?

if stats[:running] > 0
mark_message "Puma is running"
else
mark_failure
mark_message "Puma is not running"
end

if stats[:backlog] < threshold
mark_message "Backlog ok"
else
mark_failure
mark_message "Backlog congested"
end
end

def stats
return nil unless applicable?
server = Puma::Server.current
return nil if server.nil?

{
backlog: server.backlog || 0,
running: server.running || 0,
pool_capacity: server.pool_capacity || 0,
max_threads: server.max_threads || 0
}
end

def applicable?
return @applicable unless @applicable.nil?

@applicable = Object.const_defined?("Puma::Server") && !Puma::Server.current.nil?
end
end

# Register delayed_job backed up test
dj_max = OpenProject::Configuration.health_checks_jobs_queue_count_threshold
OkComputer::Registry.register "delayed_jobs_backed_up",
Expand All @@ -28,8 +75,11 @@ def check
OkComputer::Registry.register "delayed_jobs_never_ran",
DelayedJobNeverRanCheck.new(dj_never_ran_max)

backlog_threshold = OpenProject::Configuration.health_checks_backlog_threshold
OkComputer::Registry.register "puma", PumaCheck.new(backlog_threshold)

# Make dj backed up optional due to bursts
OkComputer.make_optional %w(delayed_jobs_backed_up)
OkComputer.make_optional %w(delayed_jobs_backed_up puma)

# Register web worker check for web + database
OkComputer::CheckCollection.new('web').tap do |collection|
Expand All @@ -45,6 +95,7 @@ def check
collection.register :mail, OkComputer::ActionMailerCheck.new
collection.register :delayed_jobs_backed_up, OkComputer::Registry.fetch('delayed_jobs_backed_up')
collection.register :delayed_jobs_never_ran, OkComputer::Registry.fetch('delayed_jobs_never_ran')
collection.register :puma, OkComputer::Registry.fetch('puma')
OkComputer::Registry.default_collection.register 'full', collection
end

Expand Down
30 changes: 30 additions & 0 deletions config/initializers/log_slow_sql_queries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
OpenProject::Application.configure do
config.after_initialize do
slow_sql_threshold = OpenProject::Configuration.sql_slow_query_threshold.to_i
return if slow_sql_threshold == 0

ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, start, finish, _id, data|
# Skip transaction that may be blocked
next if data[:sql].match(/BEGIN|COMMIT/)

# Skip smaller durations
duration = ((finish - start) * 1000).round(4)
next if duration <= slow_sql_threshold

payload = {
duration: duration,
time: start.iso8601,
cached: !!data[:cache],
sql: data[:sql],
}

sql_log_string = data[:sql].strip.gsub(/(^([\s]+)?$\n)/, "")
OpenProject.logger.warn "Encountered slow SQL (#{payload[:duration]} ms): #{sql_log_string}",
payload: payload,
# Hash of the query for reference/fingerprinting
reference: Digest::SHA1.hexdigest(data[:sql])
rescue StandardError => e
OpenProject.logger.error "Failed to record slow SQL query: #{e}"
end
end
end
22 changes: 22 additions & 0 deletions db/migrate/20200625133727_fix_inherited_group_member_roles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class FixInheritedGroupMemberRoles < ActiveRecord::Migration[6.0]
def up
# Delete all member roles that should be inherited by groups
MemberRole.where.not(inherited_from: nil).delete_all

# For all group memberships, recreate the member_roles for all users
# which will auto-create members for the users if necessary
MemberRole
.joins(member: [:principal])
.includes(member: %i[principal member_roles])
.where("#{Principal.table_name}.type" => 'Group')
.find_each do |member_role|

# Recreate member_roles for all group members
member_role.send :add_role_to_group_users
end
end

def down
# Nothing to do
end
end
9 changes: 5 additions & 4 deletions docs/api/apiv3/endpoints/time_entries.apib
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Group Time Entries

## Actions
| Link | Description | Condition |
|:-------------------:| -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| updateImmediately | Directly perform edits on this time entry | **Permission**: 'edit time entries' or 'edit own time entries if the time entry belongs to the user |
| delete | Delete this time entry | **Permission**: 'edit time entries' or 'edit own time entries if the time entry belongs to the user |
| Link | Description | Condition |
|:-------------------:| -------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| updateImmediately | Directly perform edits on this time entry | **Permission**: 'edit time entries' or 'edit own time entries' if the time entry belongs to the user |
| update | Form endpoint that aids in preparing and performing edits on a TimeEntry | **Permission**: 'edit time entries' or 'edit own time entries' if the time entry belongs to the user |
| delete | Delete this time entry | **Permission**: 'edit time entries' or 'edit own time entries' if the time entry belongs to the user |

## Linked Properties
| Link | Description | Type | Constraints | Supported operations | Condition |
Expand Down
1 change: 1 addition & 0 deletions docs/installation-and-operations/installation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ OpenProject can be setup in three different ways:
| [Installation with DEB/RPM packages](./packaged) | This is the recommended way to install OpenProject |
| [Installation with docker](./docker) | This allows to setup OpenProject in an isolated manner using Docker |
| [Installation with Univention Corporate Server](./univention) | OpenProject is available in the App Center and comes integrated with the identity management |
| [Other](misc/) | Extra information on installing OpenProject on specific platforms such as Kubernetes. |

**We recommend downloading the DEB/RPM package installation.**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,16 @@ data will be stored across container restarts, and start the container with
those directories mounted:

```bash
sudo mkdir -p /var/lib/openproject/{pgdata,static}
sudo mkdir -p /var/lib/openproject/{pgdata,assets}

docker run -d -p 8080:80 --name openproject -e SECRET_KEY_BASE=secret \
-v /var/lib/openproject/pgdata:/var/openproject/pgdata \
-v /var/lib/openproject/static:/var/openproject/assets \
-v /var/lib/openproject/assets:/var/openproject/assets \
openproject/community:10
```

**Note**: Make sure to replace `secret` with a random string. One way to generate one is to run `head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''` if you are on Linux.

Since we named the container, you can now stop it by running:

```bash
Expand Down
12 changes: 12 additions & 0 deletions docs/installation-and-operations/installation/kubernetes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
sidebar_navigation: false
---

# Kubernetes

Kubernetes is a container orchestration tool. As such it can use the
OpenProject docker container in the same manner as shown in the [docker section](../docker/#recommended-usage).

You can translate OpenProject's [`docker-compose.yml`](https://github.com/opf/openproject/blob/stable/10/docker-compose.yml)
for use in Kubernetes using [Kompose](https://github.com/kubernetes/kompose)
as described in the Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/).
Loading

0 comments on commit 356ed62

Please sign in to comment.