Placeholder user project members (#8961)

* remove intermediate placeholder scope

Doing so, placeholder users will begin to show up in the system

* remove scope without value

* extract scope

* use enum for status

* allow placeholder users to become project members

* display placeholder user member on members widget

* remove now superfluous method

The status name can simply be queried via #status now

* replace possible_assignees/responsibles on project

This also leads to placeholder users becoming eligible as assignees and
responsibles.

* fix aggregated scope on bulk edit

* linting

* remove IssueHelper
pull/8971/head
ulferts 4 years ago committed by GitHub
parent 29eed9d536
commit 5ea185ef66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/cells/members/row_cell.rb
  2. 2
      app/cells/users/row_cell.rb
  3. 9
      app/contracts/members/create_contract.rb
  4. 15
      app/contracts/work_packages/base_contract.rb
  5. 2
      app/controllers/concerns/user_invitation.rb
  6. 13
      app/controllers/members_controller.rb
  7. 5
      app/controllers/users_controller.rb
  8. 50
      app/controllers/work_packages/bulk_controller.rb
  9. 4
      app/helpers/application_helper.rb
  10. 64
      app/helpers/issues_helper.rb
  11. 4
      app/helpers/users_helper.rb
  12. 6
      app/helpers/work_packages_helper.rb
  13. 1
      app/mailers/base_mailer.rb
  14. 2
      app/models/custom_actions/actions/assigned_to.rb
  15. 2
      app/models/custom_actions/actions/notify.rb
  16. 2
      app/models/custom_actions/actions/responsible.rb
  17. 2
      app/models/deleted_user.rb
  18. 4
      app/models/mail_handler.rb
  19. 4
      app/models/members/scopes/not_locked.rb
  20. 81
      app/models/principal.rb
  21. 42
      app/models/principals/scopes/human.rb
  22. 53
      app/models/principals/scopes/like.rb
  23. 45
      app/models/principals/scopes/not_builtin.rb
  24. 52
      app/models/principals/scopes/possible_assignee.rb
  25. 49
      app/models/principals/scopes/possible_member.rb
  26. 40
      app/models/principals/scopes/user.rb
  27. 55
      app/models/project.rb
  28. 4
      app/models/queries/filters/shared/user_status_filter.rb
  29. 2
      app/models/queries/members/filters/principal_filter.rb
  30. 1
      app/models/queries/principals/filters/member_filter.rb
  31. 2
      app/models/queries/principals/filters/status_filter.rb
  32. 4
      app/models/queries/principals/filters/type_filter.rb
  33. 2
      app/models/queries/users/filters/user_filter.rb
  34. 7
      app/models/queries/users/user_query.rb
  35. 2
      app/models/queries/work_packages/filter/principal_loader.rb
  36. 60
      app/models/user.rb
  37. 6
      app/models/users/inexistent_user.rb
  38. 39
      app/models/users/scopes/newest.rb
  39. 14
      app/models/users/status_options.rb
  40. 3
      app/models/watcher.rb
  41. 10
      app/models/work_package.rb
  42. 2
      app/seeders/admin_user_seeder.rb
  43. 2
      app/seeders/development_data/users_seeder.rb
  44. 2
      app/services/authorization/user_allowed_service.rb
  45. 19
      app/services/projects/copy/work_packages_dependent_service.rb
  46. 17
      app/services/user_search_service.rb
  47. 2
      app/views/categories/_form.html.erb
  48. 2
      app/views/groups/_users.html.erb
  49. 2
      app/views/user_mailer/work_package_watcher_changed.html.erb
  50. 2
      app/views/user_mailer/work_package_watcher_changed.text.erb
  51. 4
      app/views/work_packages/moves/new.html.erb
  52. 4
      db/migrate/20191106132533_make_system_user_active.rb
  53. 8
      db/migrate/20191112111040_fix_system_user_status.rb
  54. 2
      lib/api/v3/memberships/schemas/membership_schema_representer.rb
  55. 41
      lib/api/v3/placeholder_users/placeholder_user_representer.rb
  56. 42
      lib/api/v3/principals/associated_subclass_lambda.rb
  57. 4
      lib/api/v3/principals/not_builtin_elements.rb
  58. 4
      lib/api/v3/principals/principal_representer.rb
  59. 13
      lib/api/v3/projects/available_assignees_api.rb
  60. 13
      lib/api/v3/projects/available_responsibles_api.rb
  61. 2
      lib/api/v3/queries/schemas/all_principals_filter_dependency_representer.rb
  62. 2
      lib/api/v3/queries/schemas/user_filter_dependency_representer.rb
  63. 2
      lib/api/v3/users/paginated_user_collection_representer.rb
  64. 2
      lib/api/v3/users/user_collection_representer.rb
  65. 4
      lib/api/v3/users/user_representer.rb
  66. 2
      lib/api/v3/utilities/custom_field_injector.rb
  67. 3
      lib/api/v3/utilities/path_helper.rb
  68. 2
      lib/api/v3/work_packages/principal_setter.rb
  69. 2
      lib/api/v3/work_packages/watchers_api.rb
  70. 4
      lib/open_project/enterprise.rb
  71. 21
      lib/plugins/acts_as_watchable/lib/acts_as_watchable.rb
  72. 2
      modules/backlogs/app/views/rb_taskboards/show.html.erb
  73. 3
      modules/bim/app/representers/bim/bcf/api/v2_1/project_extensions/representer.rb
  74. 9
      modules/bim/spec/representers/bcf/api/v2_1/project_extensions/representer_spec.rb
  75. 2
      modules/budgets/app/models/budget.rb
  76. 2
      modules/budgets/app/views/budgets/items/_labor_budget_item.html.erb
  77. 5
      modules/costs/app/helpers/costlog_helper.rb
  78. 8
      modules/dashboards/spec/features/members_spec.rb
  79. 2
      modules/reporting/app/models/cost_query/filter/user_id.rb
  80. 9
      spec/contracts/work_packages/base_contract_spec.rb
  81. 3
      spec/contracts/work_packages/create_contract_spec.rb
  82. 95
      spec/contracts/work_packages/shared_base_contract.rb
  83. 15
      spec/contracts/work_packages/update_contract_spec.rb
  84. 6
      spec/controllers/account_controller_spec.rb
  85. 2
      spec/controllers/concerns/auth_source_sso_spec.rb
  86. 6
      spec/controllers/users_controller_spec.rb
  87. 6
      spec/factories/user_factory.rb
  88. 2
      spec/features/custom_fields/multi_user_custom_field_spec.rb
  89. 20
      spec/features/members/membership_filter_spec.rb
  90. 169
      spec/features/members/membership_spec.rb
  91. 4
      spec/features/users/index_spec.rb
  92. 2
      spec/features/users/self_registration_spec.rb
  93. 16
      spec/features/work_packages/edit_work_package_spec.rb
  94. 12
      spec/helpers/users_helper_spec.rb
  95. 39
      spec/lib/api/v3/groups/group_representer_spec.rb
  96. 4
      spec/lib/api/v3/memberships/schemas/membership_schema_representer_spec.rb
  97. 173
      spec/lib/api/v3/placeholder_users/placeholder_user_representer_rendering_spec.rb
  98. 2
      spec/lib/api/v3/queries/query_representer_generation_spec.rb
  99. 8
      spec/lib/api/v3/users/user_representer_spec.rb
  100. 2
      spec/lib/api/v3/utilities/custom_field_injector_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,5 +1,7 @@
module Members
class RowCell < ::RowCell
include UsersHelper
property :principal
def member
@ -74,7 +76,7 @@ module Members
end
def status
I18n.t("status_#{model.principal.status_name}")
translate_user_status(model.principal.status)
end
def may_update?

@ -8,7 +8,7 @@ module Users
end
def row_css_class
status = %w(anon active registered locked)[user.status]
status = user.status
blocked = "blocked" if user.failed_too_many_recent_login_attempts?
["user", status, blocked].compact.join(" ")

@ -28,19 +28,24 @@
module Members
class CreateContract < BaseContract
include AssignableValuesContract
attribute :project
attribute :user_id
attribute :principal do
principal_assignable
end
def assignable_principals
Principal.possible_member(project)
end
private
def principal_assignable
return if principal.nil?
# Only users have the `locked?` shorthand
if principal.builtin? || principal.status == Principal::STATUSES[:locked]
if principal.builtin? || principal.locked?
errors.add(:principal, :unassignable)
end
end

@ -70,7 +70,7 @@ module WorkPackages
validate_people_visible :assigned_to,
'assigned_to_id',
model.project.possible_assignee_members
assignable_assignees
end
attribute :responsible_id do
@ -78,7 +78,7 @@ module WorkPackages
validate_people_visible :responsible,
'responsible_id',
model.project.possible_responsible_members
assignable_responsibles
end
attribute :schedule_manually
@ -178,6 +178,15 @@ module WorkPackages
model.project&.budgets
end
def assignable_assignees
if model.project
Principal.possible_assignee(model.project)
else
Principal.none
end
end
alias_method :assignable_responsibles, :assignable_assignees
private
attr_reader :can
@ -298,7 +307,7 @@ module WorkPackages
end
def principal_visible?(id, list)
list.exists?(user_id: id)
list.exists?(id: id)
end
def start_before_soonest_start?

@ -59,7 +59,7 @@ module UserInvitation
login: login,
firstname: first_name,
lastname: last_name,
status: Principal::STATUSES[:invited]
status: Principal.statuses[:invited]
assign_user_attributes(user)

@ -102,9 +102,7 @@ class MembersController < ApplicationController
end
def autocomplete_for_member
@principals = Principal
.possible_members(params[:q], 100)
.where.not(id: @project.principals)
@principals = possible_members(params[:q], 100)
@email = suggest_invite_via_email? current_user,
params[:q],
@ -165,7 +163,14 @@ class MembersController < ApplicationController
def set_roles_and_principles!
@roles = Role.givable
# Check if there is at least one principal that can be added to the project
@principals_available = @project.possible_members('', 1)
@principals_available = possible_members('', 1)
end
def possible_members(criteria, limit)
Principal
.possible_member(@project)
.like(criteria)
.limit(limit)
end
def index_members

@ -208,8 +208,7 @@ class UsersController < ApplicationController
@user.activate
end
# Was the account activated? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUSES[:registered],
User::STATUSES[:active]])
was_activated = (@user.status_change == %w[registered active])
if params[:activate] && @user.missing_authentication_method?
flash[:error] = I18n.t(:error_status_change_failed,
@ -229,7 +228,7 @@ class UsersController < ApplicationController
end
def resend_invitation
status = Principal::STATUSES[:invited]
status = Principal.statuses[:invited]
@user.update status: status if @user.status != status
token = UserInvitation.reinvite_user @user.id

@ -35,7 +35,6 @@ class WorkPackages::BulkController < ApplicationController
include CustomFieldsHelper
include RelationsHelper
include QueriesHelper
include IssuesHelper
def edit
setup_edit
@ -57,28 +56,25 @@ class WorkPackages::BulkController < ApplicationController
end
def destroy
unless WorkPackage.cleanup_associated_before_destructing_if_required(@work_packages, current_user, params[:to_do])
if WorkPackage.cleanup_associated_before_destructing_if_required(@work_packages, current_user, params[:to_do])
destroy_work_packages(@work_packages)
respond_to do |format|
format.html do
render locals: { work_packages: @work_packages,
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
redirect_back_or_default(project_work_packages_path(@work_packages.first.project))
end
format.json do
render json: { error_message: 'Clean up of associated objects required' }, status: 420
head :ok
end
end
else
destroy_work_packages(@work_packages)
respond_to do |format|
format.html do
redirect_back_or_default(project_work_packages_path(@work_packages.first.project))
render locals: { work_packages: @work_packages,
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
end
format.json do
head :ok
render json: { error_message: 'Clean up of associated objects required' }, status: 420
end
end
end
@ -87,24 +83,28 @@ class WorkPackages::BulkController < ApplicationController
private
def setup_edit
@available_statuses = @projects.map { |p| Workflow.available_statuses(p) }.inject { |memo, w| memo & w }
@custom_fields = @projects.map(&:all_work_package_custom_fields).inject { |memo, c| memo & c }
@assignables = @projects.map(&:possible_assignees).inject { |memo, a| memo & a }
@responsibles = @projects.map(&:possible_responsibles).inject { |memo, a| memo & a }
@types = @projects.map(&:types).inject { |memo, t| memo & t }
@available_statuses = @projects.map { |p| Workflow.available_statuses(p) }.inject(&:&)
@custom_fields = @projects.map(&:all_work_package_custom_fields).inject(&:&)
@assignables = possible_assignees
@responsibles = @assignables
@types = @projects.map(&:types).inject(&:&)
end
def destroy_work_packages(work_packages)
work_packages.each do |work_package|
begin
WorkPackages::DeleteService
.new(user: current_user,
model: work_package.reload)
.call
rescue ::ActiveRecord::RecordNotFound
# raised by #reload if work package no longer exists
# nothing to do, work package was already deleted (eg. by a parent)
end
WorkPackages::DeleteService
.new(user: current_user,
model: work_package.reload)
.call
rescue ::ActiveRecord::RecordNotFound
# raised by #reload if work package no longer exists
# nothing to do, work package was already deleted (eg. by a parent)
end
end
def possible_assignees
@projects.inject(Principal.all) do |scope, project|
scope.where(id: Principal.possible_assignee(project))
end
end

@ -86,11 +86,11 @@ module ApplicationHelper
# returns a class name based on the user's status
def user_status_class(user)
'status_' + user.status_name
'status_' + user.status
end
def user_status_i18n(user)
t "status_#{user.status_name}"
t "status_#{user.status}"
end
def delete_link(url, options = {})

@ -1,64 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
module IssuesHelper
include ApplicationHelper
# Renders a HTML/CSS tooltip
#
# To use, a trigger div is needed. This is a div with the class of "tooltip"
# that contains this method wrapped in a span with the class of "tip"
#
# <div class="tooltip"><%= link_to_issue(issue) %>
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
# </div>
#
def render_issue_tooltip(issue)
@cached_label_status ||= WorkPackage.human_attribute_name(:status)
@cached_label_start_date ||= WorkPackage.human_attribute_name(:start_date)
@cached_label_due_date ||= WorkPackage.human_attribute_name(:due_date)
@cached_label_assigned_to ||= WorkPackage.human_attribute_name(:assigned_to)
@cached_label_priority ||= WorkPackage.human_attribute_name(:priority)
@cached_label_project ||= WorkPackage.human_attribute_name(:project)
(link_to_work_package(issue) + "<br /><br />
<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />
<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />
<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />
<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />
<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />
<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe)
end
def last_issue_note(issue)
note_journals = issue.journals.select(&:notes?)
return t(:text_no_notes) if note_journals.empty?
note_journals.last.notes
end
end

@ -52,7 +52,7 @@ module UsersHelper
def full_user_status(user, include_num_failed_logins = false)
user_status = ''
unless user.active?
user_status = translate_user_status(user.status_name)
user_status = translate_user_status(user.status)
end
brute_force_status = ''
if user.failed_too_many_recent_login_attempts?
@ -88,7 +88,7 @@ module UsersHelper
# Create buttons to lock/unlock a user and reset failed logins
def build_change_user_status_action(user)
status = user.status_name.to_sym
status = user.status.to_sym
blocked = !!user.failed_too_many_recent_login_attempts?
result = ''.html_safe

@ -212,6 +212,12 @@ module WorkPackagesHelper
route[:controller] == 'work_packages' && route[:action] == 'index' && route[:state]&.match?(/^\d+/)
end
def last_work_package_note(work_package)
note_journals = work_package.journals.select(&:notes?)
return t(:text_no_notes) if note_journals.empty?
note_journals.last.notes
end
private
def truncated_work_package_description(work_package, lines = 3)

@ -31,7 +31,6 @@ class BaseMailer < ActionMailer::Base
helper :application, # for format_text
:work_packages, # for css classes
:custom_fields # for show_value
helper IssuesHelper
include OpenProject::LocaleHelper

@ -53,7 +53,7 @@ class CustomActions::Actions::AssignedTo < CustomActions::Actions::Base
def available_principles
principal_class
.active_or_registered
.not_locked
.select(:id, :firstname, :lastname, :type)
.order_by_name
.map { |u| [u.id, u.name] }

@ -62,7 +62,7 @@ class CustomActions::Actions::Notify < CustomActions::Actions::Base
def principals
Principal
.active_or_registered
.not_locked
.select(:id, :firstname, :lastname, :type)
.order_by_name
end

@ -31,7 +31,7 @@ class CustomActions::Actions::Responsible < CustomActions::Actions::Base
def associated
User
.active_or_registered
.not_locked
.select(:id, :firstname, :lastname, :type)
.order_by_name
.map { |u| [u.id, u.name] }

@ -7,7 +7,7 @@ class DeletedUser < User
end
def self.first
super || create(type: to_s, status: STATUSES[:locked])
super || create(type: to_s, status: statuses[:locked])
end
# Overrides a few properties

@ -481,9 +481,9 @@ class MailHandler < ActionMailer::Base
end
end
def find_assignee_from_keyword(keyword, issue)
def find_assignee_from_keyword(keyword, work_package)
keyword = keyword.to_s.downcase
assignable = issue.assignable_assignees
assignable = Principal.possible_assignee(work_package.project)
assignee = nil
assignee ||= assignable.detect do |a|
[a.mail.to_s.downcase, a.login.to_s.downcase].include?(keyword)

@ -28,14 +28,14 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
# Find all members that are whose principals are not locked.
# Find all members whose principals are not locked.
module Members::Scopes
class NotLocked
def self.fetch
Member
.includes(:principal)
.references(:principals)
.merge(Principal.not_builtin_but_with_placeholder_users.active_or_registered)
.merge(Principal.not_locked, rewhere: true)
end
end
end

@ -29,9 +29,10 @@
#++
class Principal < ApplicationRecord
include ::Scopes::Scoped
# Account statuses
# Code accessing the keys assumes they are ordered, which they are since Ruby 1.9
STATUSES = {
enum status: {
active: 1,
registered: 2,
locked: 3,
@ -56,14 +57,17 @@ class Principal < ApplicationRecord
has_many :projects, through: :memberships
has_many :categories, foreign_key: 'assigned_to_id', dependent: :nullify
scope :active, -> { where(status: STATUSES[:active]) }
scope_classes Principals::Scopes::NotBuiltin,
Principals::Scopes::User,
Principals::Scopes::Human,
Principals::Scopes::Like,
Principals::Scopes::PossibleMember,
Principals::Scopes::PossibleAssignee
scope :active_or_registered, -> {
not_builtin.where(status: [STATUSES[:active], STATUSES[:registered], STATUSES[:invited]])
scope :not_locked, -> {
not_builtin.where.not(status: statuses[:locked])
}
scope :active_or_registered_like, ->(query) { active_or_registered.like(query) }
scope :in_project, ->(project) {
where(id: Member.of(project).select(:user_id))
}
@ -72,48 +76,14 @@ class Principal < ApplicationRecord
where.not(id: Member.of(project).select(:user_id))
}
scope :not_builtin, -> {
# TODO: Remove PlaceholderUser from this list. This is a temporary hack that ensures that Placeholders don't
# suddenly show up where they are are not supposed to show up. In case you want them to show up use the temporary
# scope :not_builtin_but_with_placeholder_users
where.not(type: [SystemUser.name, AnonymousUser.name, DeletedUser.name, PlaceholderUser.name])
}
scope :not_builtin_but_with_placeholder_users, -> {
# TODO: This is temporary precaution scope to circumvent the hack in the :not_builtin scope. Needs to be
# removed before we release this code.
where.not(type: [SystemUser.name, AnonymousUser.name, DeletedUser.name])
}
OpenProject::Deprecation.deprecate_class_method self,
:not_builtin_but_with_placeholder_users,
:not_builtin
scope :like, ->(q) {
firstnamelastname = "((firstname || ' ') || lastname)"
lastnamefirstname = "((lastname || ' ') || firstname)"
s = "%#{q.to_s.downcase.strip.tr(',', '')}%"
where(['LOWER(login) LIKE :s OR ' +
"LOWER(#{firstnamelastname}) LIKE :s OR " +
"LOWER(#{lastnamefirstname}) LIKE :s OR " +
'LOWER(mail) LIKE :s',
{ s: s }])
.order(:type, :login, :lastname, :firstname, :mail)
}
before_create :set_default_empty_values
def name(_formatter = nil)
to_s
end
def self.possible_members(criteria, limit)
Principal.active_or_registered_like(criteria).limit(limit)
end
def self.search_scope_without_project(project, query)
active_or_registered_like(query).not_in_project(project)
not_locked.like(query).not_in_project(project)
end
def self.order_by_name
@ -133,20 +103,6 @@ class Principal < ApplicationRecord
.or(me)
end
def status_name
# Only Users should have another status than active.
# User defines the status values and other classes like Principal
# shouldn't know anything about them. Nevertheless, some functions
# want to know the status for other Principals than User.
raise 'Principal has status other than active' unless status == STATUSES[:active]
'active'
end
def active_or_registered?
[STATUSES[:active], STATUSES[:registered], STATUSES[:invited]].include?(status)
end
# Helper method to identify internal users
def builtin?
false
@ -177,6 +133,19 @@ class Principal < ApplicationRecord
end
end
class << self
# Hack to exclude the Users::InexistentUser
# from showing up on filters for type.
# The method is copied over from rails changed only
# by the #compact call.
def type_condition(table = arel_table)
sti_column = table[inheritance_column]
sti_names = ([self] + descendants).map(&:sti_name).compact
predicate_builder.build(sti_column, sti_names)
end
end
protected
# Make sure we don't try to insert NULL values (see #4632)

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Only return Principals that are, direct or indirect humans.
# Includes
# * User
# * Group
module Principals::Scopes
class Human
def self.fetch
Principal.where(type: [::User.name,
Group.name])
end
end
end

@ -0,0 +1,53 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Returns principals whose
# * login
# * firstname
# * lastname
# matches the provided string
module Principals::Scopes
class Like
def self.fetch(search_string)
firstnamelastname = "((firstname || ' ') || lastname)"
lastnamefirstname = "((lastname || ' ') || firstname)"
s = "%#{search_string.to_s.downcase.strip.tr(',', '')}%"
Principal
.where(['LOWER(login) LIKE :s OR ' +
"LOWER(#{firstnamelastname}) LIKE :s OR " +
"LOWER(#{lastnamefirstname}) LIKE :s OR " +
'LOWER(mail) LIKE :s',
{ s: s }])
.order(:type, :login, :lastname, :firstname, :mail)
end
end
end

@ -0,0 +1,45 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Only return Principals that are not built into the system so only return those that where
# created by a human.
# Excludes
# * DeletedUser
# * SystemUser
# * AnonymousUser
module Principals::Scopes
class NotBuiltin
def self.fetch
Principal.where.not(type: [SystemUser.name,
AnonymousUser.name,
DeletedUser.name])
end
end
end

@ -0,0 +1,52 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
module Principals::Scopes
class PossibleAssignee
# Returns principals eligible to be assigned to a work package as:
# * assignee
# * responsible
# Those principals can be of class
# * User
# * PlaceholderUser
# * Group
# User instances need to be non locked (status).
# Only principals with a role marked as assignable in the project are returned.
# @project [Project] The project for which eligible candidates are to be searched
# @return [ActiveRecord::Relation] A scope of eligible candidates
def self.fetch(project)
Principal
.not_locked
.includes(:members)
.references(:members)
.merge(Member.assignable.of(project))
end
end
end

@ -0,0 +1,49 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
module Principals::Scopes
class PossibleMember
# Returns principals eligible to become project members. Those principals can be of class
# * User
# * PlaceholderUser
# * Group
# User instances need to be non locked (status)
# Principals which already are project members are are returned.
# @project [Project] The project for which eligible candidates are to be searched
# @return [ActiveRecord::Relation] A scope of eligible candidates
def self.fetch(project)
Queries::Principals::PrincipalQuery
.new(user: ::User.current)
.where(:member, '!', [project.id])
.where(:status, '!', [Principal.statuses[:locked]])
.results
end
end
end

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Only return Principals that are of type User
module Principals::Scopes
class User
def self.fetch
# Have to use the User model here so that the scopes defined on User
# are also available after the scope is used.
::User.where(type: [::User.name])
end
end
end

@ -42,59 +42,14 @@ class Project < ApplicationRecord
# reserved identifiers
RESERVED_IDENTIFIERS = %w(new).freeze
# TODO: Is this association ever used? Groups are missing (and PlaceholderUsers).
has_many :members, -> {
# TODO: check whether this should
# remaint to be limited to User only
includes(:principal, :roles)
.where(
"#{Principal.table_name}.type='User' AND (
#{User.table_name}.status=#{Principal::STATUSES[:active]} OR
#{User.table_name}.status=#{Principal::STATUSES[:invited]}
)"
)
.merge(Principal.not_locked.user)
.references(:principal, :roles)
}
has_many :possible_assignee_members,
-> { assignable },
class_name: 'Member'
# Read only
has_many :possible_assignees,
->(object) {
# Have to reference members and roles again although
# possible_assignee_members does already specify it to be able to use the
# assignable_principals there
#
# The .where(members_users: { project_id: object.id })
# part is an optimization preventing to have all the members joined
includes(members: :roles)
.where(members_users: { project_id: object.id })
.references(:roles)
.merge(Principal.order_by_name)
},
through: :possible_assignee_members,
source: :principal
has_many :possible_responsible_members,
-> { assignable },
class_name: 'Member'
# Read only
has_many :possible_responsibles,
->(object) {
# Have to reference members and roles again although
# possible_responsible_members does already specify it to be able to use
# assignable_principals there
#
# The .where(members_users: { project_id: object.id })
# part is an optimization preventing to have all the members joined
includes(members: :roles)
.where(members_users: { project_id: object.id })
.references(:roles)
.merge(Principal.order_by_name)
},
through: :possible_responsible_members,
source: :principal
has_many :memberships, class_name: 'Member'
has_many :member_principals,
-> { not_locked },
@ -203,10 +158,6 @@ class Project < ApplicationRecord
visible.like(query)
end
def possible_members(criteria, limit)
Principal.active_or_registered.like(criteria).not_in_project(self).limit(limit)
end
def add_member(user, roles)
members.build.tap do |m|
m.principal = user

@ -36,7 +36,7 @@ module Queries::Filters::Shared::UserStatusFilter
module InstanceMethods
def allowed_values
Principal::STATUSES.keys.map do |key|
Principal.statuses.keys.map do |key|
[I18n.t(:"status_#{key}"), key]
end
end
@ -46,7 +46,7 @@ module Queries::Filters::Shared::UserStatusFilter
end
def status_values
values.map { |value| Principal::STATUSES[value.to_sym] }
values.map { |value| Principal.statuses[value.to_sym] }
end
def where

@ -34,7 +34,7 @@ class Queries::Members::Filters::PrincipalFilter < Queries::Members::Filters::Me
def allowed_values
@allowed_values ||= begin
values = Principal
.active_or_registered
.not_locked
.in_visible_project_or_me
.map { |s| [s.name, s.id.to_s] }
.sort

@ -50,6 +50,7 @@ class Queries::Principals::Filters::MemberFilter < Queries::Principals::Filters:
default_scope.where(members: { project_id: values })
when '!'
default_scope.where.not(members: { project_id: values })
.or(default_scope.where(members: { project_id: nil }))
when '*'
default_scope.where.not(members: { project_id: nil })
when '!*'

@ -29,7 +29,7 @@
class Queries::Principals::Filters::StatusFilter < Queries::Principals::Filters::PrincipalFilter
def allowed_values
::Principal::STATUSES.map do |key, value|
::Principal.statuses.map do |key, value|
[key, value]
end
end

@ -29,8 +29,8 @@
class Queries::Principals::Filters::TypeFilter < Queries::Principals::Filters::PrincipalFilter
def allowed_values
[[Group.to_s, Group.to_s],
[User.to_s, User.to_s]]
[User, Group, PlaceholderUser]
.map { |x| [x.to_s, x.to_s] }
end
def type

@ -28,7 +28,7 @@
#++
class Queries::Users::Filters::UserFilter < Queries::Filters::Base
self.model = User
self.model = User.user
def human_name
User.human_attribute_name(name)

@ -32,6 +32,11 @@ class Queries::Users::UserQuery < Queries::BaseQuery
end
def default_scope
User.not_builtin
# This seemingly duplication is necessary because of the builtin classes
# * SystemUser
# * DeletedUser
# * AnonymousUser
# inheriting from user. Without it, instances of those classes would show up.
User.user
end
end

@ -55,7 +55,7 @@ class Queries::WorkPackages::Filter::PrincipalLoader
if project
project.principals.sort
else
Principal.active_or_registered.in_visible_project.sort
Principal.not_locked.in_visible_project.sort
end
end

@ -31,8 +31,6 @@
require 'digest/sha1'
class User < Principal
include ::Scopes::Scoped
USER_FORMATS_STRUCTURE = {
firstname_lastname: [:firstname, :lastname],
firstname: [:firstname],
@ -58,6 +56,7 @@ class User < Principal
].freeze
include ::Associations::Groupable
extend DeprecatedAlias
has_many :categories, foreign_key: 'assigned_to_id',
dependent: :nullify
@ -88,7 +87,8 @@ class User < Principal
scope :blocked, -> { create_blocked_scope(self, true) }
scope :not_blocked, -> { create_blocked_scope(self, false) }
scope_classes Users::Scopes::FindByLogin
scope_classes Users::Scopes::FindByLogin,
Users::Scopes::Newest
def self.create_blocked_scope(scope, blocked)
scope.where(blocked_condition(blocked))
@ -160,8 +160,6 @@ class User < Principal
scope :admin, -> { where(admin: true) }
scope :newest, -> { not_builtin.order(created_at: :desc) }
def self.unique_attribute
:login
end
@ -318,23 +316,8 @@ class User < Principal
# Return user's authentication provider for display
def authentication_provider
return if identity_url.blank?
identity_url.split(':', 2).first.titleize
end
def status_name
STATUSES.invert[status].to_s
end
def active?
status == STATUSES[:active]
end
def registered?
status == STATUSES[:registered]
end
def locked?
status == STATUSES[:locked]
identity_url.split(':', 2).first.titleize
end
##
@ -347,40 +330,25 @@ class User < Principal
alias_method :activatable?, :locked?
def activate
self.status = STATUSES[:active]
self.status = self.class.statuses[:active]
end
def register
self.status = STATUSES[:registered]
self.status = self.class.statuses[:registered]
end
def invite
self.status = STATUSES[:invited]
self.status = self.class.statuses[:invited]
end
def lock
self.status = STATUSES[:locked]
end
def activate!
update_attribute(:status, STATUSES[:active])
end
def register!
update_attribute(:status, STATUSES[:registered])
self.status = self.class.statuses[:locked]
end
def invite!
update_attribute(:status, STATUSES[:invited])
end
def invited?
status == STATUSES[:invited]
end
def lock!
update_attribute(:status, STATUSES[:locked])
end
deprecated_alias :activate!, :active!
deprecated_alias :register!, :registered!
deprecated_alias :invite!, :invited!
deprecated_alias :lock!, :locked!
# Returns true if +clear_password+ is the correct user's password, otherwise false
# If +update_legacy+ is set, will automatically save legacy passwords using the current
@ -689,7 +657,7 @@ class User < Principal
u.login = ''
u.firstname = ''
u.mail = ''
u.status = User::STATUSES[:active]
u.status = User.statuses[:active]
end).save
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
end
@ -707,7 +675,7 @@ class User < Principal
login: "",
mail: "",
admin: false,
status: User::STATUSES[:active],
status: User.statuses[:active],
first_login: false
)

@ -28,4 +28,8 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
class Users::InexistentUser < User; end
class Users::InexistentUser < User
def self.sti_name
nil
end
end

@ -0,0 +1,39 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Returns users sorted by their creation date. Inheriting classes are
# excluded.
module Users::Scopes
class Newest
def self.fetch
User.user.order(created_at: :desc)
end
end
end

@ -14,18 +14,18 @@ module Users
end
def user_count_by_status(extra: {})
counts = User.not_builtin.group(:status).count.to_hash
counts = User.user.group(:status).count.to_hash
counts
.merge(symbolic_user_counts)
.merge(extra)
.reject { |_, v| v.nil? } # remove nil counts to support dropping counts via extra
.map do |k, v|
known_status = Principal::STATUSES.detect { |_, i| i == k }
known_status = Principal.statuses.detect { |_, i| i == k }
if known_status
[known_status.first, v]
[known_status.first.to_sym, v]
else
[k, v]
[k.to_sym, v]
end
end
.to_h
@ -33,9 +33,9 @@ module Users
def symbolic_user_counts
{
blocked: User.not_builtin.blocked.count, # not_builtin to skip DeletedUser
all: User.not_builtin.count,
active: User.not_builtin.active.not_blocked.count # not_builtin to skip Anonymous and System users
blocked: User.user.blocked.count, # User.user scope to skip DeletedUser
all: User.user.count,
active: User.user.active.not_blocked.count # User.user to skip Anonymous and System users
}
end
end

@ -50,7 +50,8 @@ class Watcher < ApplicationRecord
def validate_active_user
# TODO add informative error message
return if user.blank?
errors.add :user_id, :invalid unless user.active_or_registered?
errors.add :user_id, :invalid if user.locked?
end
def validate_user_allowed_to_watch

@ -254,16 +254,6 @@ class WorkPackage < ApplicationRecord
time_entries.build(attributes)
end
# Users/groups the work_package can be assigned to
def assignable_assignees
project.possible_assignees
end
# Users the work_package can be assigned to
def assignable_responsibles
project.possible_responsibles
end
# Versions that the work_package can be assigned to
# A work_package can be assigned to:
# * any open, shared version of the project the wp belongs to

@ -55,7 +55,7 @@ class AdminUserSeeder < Seeder
user.mail = ENV['ADMIN_EMAIL'].presence || 'admin@example.net'
user.mail_notification = User::USER_MAIL_OPTION_ONLY_MY_EVENTS.first
user.language = I18n.locale.to_s
user.status = User::STATUSES[:active]
user.status = User.statuses[:active]
user.force_password_change = force_password_change?
end
end

@ -102,7 +102,7 @@ module DevelopmentData
user.firstname = login.humanize
user.lastname = 'DEV user'
user.mail = "#{login}@example.net"
user.status = User::STATUSES[:active]
user.status = User.statuses[:active]
user.language = I18n.locale
user.force_password_change = false
end

@ -113,7 +113,7 @@ class Authorization::UserAllowedService
# Only users that are not locked may be granted actions
# with the exception of a temporary-granted system user
def authorizable_user?
user.active_or_registered? || user.is_a?(SystemUser)
!user.locked? || user.is_a?(SystemUser)
end
def has_authorized_role?(action, project = nil)

@ -153,19 +153,20 @@ module Projects::Copy
end
def work_package_assigned_to_id(source_work_package)
assigned_to_id = source_work_package.assigned_to_id
return unless assigned_to_id
@assignees ||= target.possible_assignees.pluck(:id).to_set
assigned_to_id if @assignees.include?(assigned_to_id)
possible_principal_id(source_work_package.assigned_to_id,
source_work_package.project)
end
def work_package_responsible_id(source_work_package)
responsible_id = source_work_package.responsible_id
return unless responsible_id
possible_principal_id(source_work_package.responsible_id,
source_work_package.project)
end
def possible_principal_id(principal_id, project)
return unless principal_id
@responsible ||= target.possible_responsibles.pluck(:id).to_set
responsible_id if @responsible.include?(responsible_id)
@principals ||= Principal.possible_assignee(project).pluck(:id).to_set
principal_id if @principals.include?(principal_id)
end
end
end

@ -48,9 +48,9 @@ class UserSearchService
def scope
if users_only
project.nil? ? User : project.users
project.nil? ? User : project.users.user
else
project.nil? ? Principal : project.principals
project.nil? ? Principal : project.principals.human
end
end
@ -61,22 +61,23 @@ class UserSearchService
def ids_search(scope)
ids = params[:ids].split(',')
scope.not_builtin.where(id: ids)
scope.where(id: ids)
end
def query_search(scope)
scope = scope.in_group(params[:group_id].to_i) if params[:group_id].present?
c = ARCondition.new
if params[:status] == 'blocked'
case params[:status]
when 'blocked'
@status = :blocked
scope = scope.blocked
elsif params[:status] == 'all'
when 'all'
@status = :all
scope = scope.not_builtin
# No scope change necessary
else
@status = params[:status] ? params[:status].to_i : User::STATUSES[:active]
scope = scope.not_blocked if users_only && @status == User::STATUSES[:active]
@status = params[:status] ? params[:status].to_i : User.statuses[:active]
scope = scope.not_blocked if users_only && @status == User.statuses[:active]
c << ['status = ?', @status]
end

@ -34,7 +34,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div class="form--field">
<%= f.select :assigned_to_id,
@project.possible_assignees.sort.collect{|u| [u.name, u.id]},
Principal.possible_assignee(@project).order_by_name.collect{|u| [u.name, u.id]},
include_blank: true,
container_class: '-slim' %>
</div>

@ -43,7 +43,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div class="grid-content medium-6 -visible-overflow">
<% users = User
.not_builtin
.user
.active
.not_in_group(@group)
.limit(1) %>

@ -30,7 +30,7 @@ See docs/COPYRIGHT.rdoc for more details.
<hr />
<%= render partial: 'issue_details', locals: { issue: @issue } %>
<p>
<%= format_text(t(:text_latest_note, note: last_issue_note(@issue)),
<%= format_text(t(:text_latest_note, note: last_work_package_note(@issue)),
only_path: false,
object: @issue,
project: @issue.project) %>

@ -31,4 +31,4 @@ See docs/COPYRIGHT.rdoc for more details.
----------------------------------------
<%= render partial: 'issue_details', locals: { issue: @issue } %>
<%= t(:text_latest_note, note: last_issue_note(@issue)) %>
<%= t(:text_latest_note, note: last_work_package_note(@issue)) %>

@ -101,7 +101,7 @@ See docs/COPYRIGHT.rdoc for more details.
<%= styled_select_tag('assigned_to_id',
content_tag('option', t(:label_no_change_option), value: '') +
content_tag('option', t(:label_nobody), value: 'none') +
options_from_collection_for_select(@target_project.possible_assignees, :id, :name)) %>
options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %>
</div>
</div>
<div class="form--field">
@ -110,7 +110,7 @@ See docs/COPYRIGHT.rdoc for more details.
<%= styled_select_tag('responsible_id',
content_tag('option', t(:label_no_change_option), value: '') +
content_tag('option', t(:label_nobody), value: 'none') +
options_from_collection_for_select(@target_project.possible_responsibles, :id, :name)) %>
options_from_collection_for_select(Principal.possible_assignee(@target_project), :id, :name)) %>
</div>
</div>
<div class="form--field">

@ -3,12 +3,12 @@ class MakeSystemUserActive < ActiveRecord::Migration[6.0]
BUILTIN_STATUS ||= 0
def up
Principal.where(status: BUILTIN_STATUS).update_all(status: Principal::STATUSES[:active])
Principal.where(status: BUILTIN_STATUS).update_all(status: Principal.statuses[:active])
end
def down
AnonymousUser.update_all(status: BUILTIN_STATUS)
DeletedUser.update_all(status: BUILTIN_STATUS)
SystemUser.update_all(status: Principal::STATUSES[:locked])
SystemUser.update_all(status: Principal.statuses[:locked])
end
end

@ -9,18 +9,18 @@ class FixSystemUserStatus < ActiveRecord::Migration[6.0]
# wrong status (0) because we failed to update the on-the-fly
# creation of the anonymous user with the correct status.
active_users.each do |user|
user.update_all status: Principal::STATUSES[:active]
user.update_all status: Principal.statuses[:active]
end
deleted_user.update_all status: Principal::STATUSES[:active]
deleted_user.update_all status: Principal.statuses[:active]
end
def down
# reset system user to locked which would've been the state before this migration
system_user.update_all status: Principal::STATUSES[:locked]
system_user.update_all status: Principal.statuses[:locked]
# reset deleted usr to active which he would've been after the previous migration
deleted_user.update_all status: Principal::STATUSES[:active]
deleted_user.update_all status: Principal.statuses[:active]
# There is no need to update the anonymous user since it was supposed to be
# active at this point already anyway. The previous migration then makes it

@ -93,7 +93,7 @@ module API
end
def allowed_principals_filters
statuses = [Principal::STATUSES[:locked].to_s]
statuses = [Principal.statuses[:locked].to_s]
status_filter = { status: { operator: '!', values: statuses } }
filters = [status_filter]

@ -0,0 +1,41 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
module API
module V3
module PlaceholderUsers
class PlaceholderUserRepresenter < ::API::V3::Principals::PrincipalRepresenter
def _type
'PlaceholderUser'
end
end
end
end
end

@ -43,6 +43,8 @@ module API
:user
when Group
:group
when PlaceholderUser
:placeholder_user
when NilClass
# Fall back to user for unknown principal
# since we do not have a principal route.
@ -65,28 +67,32 @@ module API
instance = represented.send(name)
case instance
when User
::API::V3::Users::UserRepresenter.new(represented.send(name), current_user: current_user)
when Group
::API::V3::Groups::GroupRepresenter.new(represented.send(name), current_user: current_user)
when NilClass
nil
else
raise "undefined subclass for #{instance}"
end
representer = case instance
when User
::API::V3::Users::UserRepresenter
when Group
::API::V3::Groups::GroupRepresenter
when PlaceholderUser
::API::V3::PlaceholderUsers::PlaceholderUserRepresenter
when NilClass
nil
else
raise "undefined subclass for #{instance}"
end
representer&.new(represented.send(name), current_user: current_user)
}
end
def self.setter(name, property_name: name, namespaces: %i(groups users))
def self.setter(name, property_name: name, namespaces: %i(groups users placeholder_users))
->(fragment:, **) {
link = ::API::Decorators::LinkObject.new(represented,
property_name: property_name,
namespace: namespaces,
getter: :"#{name}_id",
setter: :"#{name}_id=")
link.from_hash(fragment)
::API::Decorators::LinkObject
.new(represented,
property_name: property_name,
namespace: namespaces,
getter: :"#{name}_id",
setter: :"#{name}_id=")
.from_hash(fragment)
}
end
end

@ -31,7 +31,7 @@
module API
module V3
module Principals
module GroupOrUserElements
module NotBuiltinElements
extend ::ActiveSupport::Concern
included do
@ -43,6 +43,8 @@ module API
::API::V3::Users::UserRepresenter
when Group
::API::V3::Groups::GroupRepresenter
when PlaceholderUser
::API::V3::PlaceholderUsers::PlaceholderUserRepresenter
else
raise "unsupported type"
end

@ -80,8 +80,8 @@ module API
end
def current_user_allowed_to_see_members?
current_user.allowed_to?(:view_members, nil, global: true) ||
current_user.allowed_to?(:manage_members, nil, global: true)
current_user.allowed_to_globally?(:view_members) ||
current_user.allowed_to_globally?(:manage_members)
end
end
end

@ -37,13 +37,12 @@ module API
authorize(:view_work_packages, global: true, user: current_user)
end
get do
available_assignees = @project.possible_assignees.includes(:preference)
self_link = api_v3_paths.available_assignees(@project.id)
Users::UserCollectionRepresenter.new(available_assignees,
self_link: self_link,
current_user: current_user)
end
get &::API::V3::Utilities::Endpoints::Index.new(model: Principal,
scope: -> {
Principal.possible_assignee(@project).includes(:preference)
},
render_representer: Users::UserCollectionRepresenter)
.mount
end
end
end

@ -37,13 +37,12 @@ module API
authorize(:view_work_packages, global: true, user: current_user)
end
get do
available_responsibles = @project.possible_responsibles.includes(:preference)
self_link = api_v3_paths.available_responsibles(@project.id)
Users::UserCollectionRepresenter.new(available_responsibles,
self_link: self_link,
current_user: current_user)
end
get &::API::V3::Utilities::Endpoints::Index.new(model: Principal,
scope: -> {
Principal.possible_assignee(@project).includes(:preference)
},
render_representer: Users::UserCollectionRepresenter)
.mount
end
end
end

@ -47,7 +47,7 @@ module API
def filter_query
params = [{ status: { operator: '!',
values: [Principal::STATUSES[:locked].to_s] } }]
values: [Principal.statuses[:locked].to_s] } }]
params << if filter.project
{ member: { operator: '=', values: [filter.project.id.to_s] } }

@ -45,7 +45,7 @@ module API
params = [{ type: { operator: '=',
values: ['User'] } },
{ status: { operator: '!',
values: [Principal::STATUSES[:locked].to_s] } }]
values: [Principal.statuses[:locked].to_s] } }]
if filter.project
params << { member: { operator: '=', values: [filter.project.id.to_s] } }

@ -38,7 +38,7 @@ module API
module V3
module Users
class PaginatedUserCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
include API::V3::Principals::GroupOrUserElements
include API::V3::Principals::NotBuiltinElements
end
end
end

@ -31,7 +31,7 @@ module API
module V3
module Users
class UserCollectionRepresenter < ::API::Decorators::UnpaginatedCollection
include API::V3::Principals::GroupOrUserElements
include API::V3::Principals::NotBuiltinElements
end
end
end

@ -138,8 +138,8 @@ module API
render_nil: true
property :status,
getter: ->(*) { status_name },
setter: ->(fragment:, represented:, **) { represented.status = User::STATUSES[fragment.to_sym] },
getter: ->(*) { status },
setter: ->(fragment:, represented:, **) { represented.status = User.statuses[fragment.to_sym] },
render_nil: true,
cache_if: -> { current_user_is_admin_or_self }

@ -362,7 +362,7 @@ module API
def allowed_users_static_filters
[{ status: { operator: '!',
values: [Principal::STATUSES[:locked].to_s] } },
values: [Principal.statuses[:locked].to_s] } },
{ type: { operator: '=', values: ['User'] } }]
end

@ -211,6 +211,9 @@ module API
"#{newses}/#{id}"
end
index :placeholder_user
show :placeholder_user
index :post
show :post

@ -37,7 +37,7 @@ module API
lambda = ::API::V3::Principals::AssociatedSubclassLambda
.setter(name,
property_name: property_name,
namespaces: %i(groups users))
namespaces: %i(groups users placeholder_users))
instance_exec(**args, &lambda)
}

@ -57,7 +57,7 @@ module API
resources :watchers do
helpers do
def watchers_collection
watchers = @work_package.watcher_users.active_or_registered
watchers = @work_package.watcher_users.merge(Principal.not_locked)
self_link = api_v3_paths.work_package_watchers(@work_package.id)
Users::UserCollectionRepresenter.new(watchers,
self_link: self_link,

@ -46,7 +46,7 @@ module OpenProject
end
def active_user_count
User.not_builtin.active.count
User.human.active.count
end
##
@ -61,7 +61,7 @@ module OpenProject
# While the active user limit has not been reached yet it would be reached
# if all registered and invited users were to activate their accounts.
def imminent_user_limit?
User.not_builtin.active_or_registered.count >= user_limit if user_limit
User.human.not_locked.count >= user_limit if user_limit
end
def fail_fast?

@ -109,18 +109,15 @@ module Redmine
# because while they have the right to be added as watchers having
# them pop up in every project would be weird.
def possible_watcher_users
# In rails 6, for reasons I did not look into, a different sql is produced
# when issuing
# User.active_or_registered.allowed_members(self.class.acts_as_watchable_permission, project)
# compared to
# User.allowed_members(self.class.acts_as_watchable_permission, project).active_or_registered
scope = if project.public?
User.allowed(self.class.acts_as_watchable_permission, project)
else
User.allowed_members(self.class.acts_as_watchable_permission, project)
end
scope.active_or_registered
active_scope = Principal.not_locked.user
allowed_scope = if project.public?
User.allowed(self.class.acts_as_watchable_permission, project)
else
User.allowed_members(self.class.acts_as_watchable_permission, project)
end
active_scope.where(id: allowed_scope)
end
# Returns an array of users that are proposed as watchers

@ -125,7 +125,7 @@ See docs/COPYRIGHT.rdoc for more details.
<div id="helpers">
<select class="assigned_to_id template" id="assigned_to_id_options">
<option value=""> </option>
<% @project.possible_assignees.each do |user| %>
<% Principal.possible_assignee(@project).each do |user| %>
<option value="<%= user.id %>" color="<%= get_backlogs_preference(user, :task_color) %>">
<%= user.name %>
</option>

@ -59,8 +59,7 @@ module Bim::Bcf::API::V2_1
property :user_id_type,
getter: ->(decorator:, **) {
decorator.with_check(%i[manage_bcf view_members]) do
# TODO: Move possible_assignees handling into wp base contract
model.project.possible_assignees.pluck(:mail)
assignable_assignees.pluck(:mail)
end
}

@ -35,11 +35,7 @@ describe Bim::Bcf::API::V2_1::ProjectExtensions::Representer, 'rendering' do
let(:status) { FactoryBot.build_stubbed(:status) }
let(:user) { FactoryBot.build_stubbed(:user) }
let(:project) do
FactoryBot.build_stubbed(:project).tap do |p|
allow(p)
.to receive(:possible_assignees)
.and_return([user])
end
FactoryBot.build_stubbed(:project)
end
let(:work_package) { FactoryBot.build_stubbed(:stubbed_work_package, project: project) }
let(:priority) { FactoryBot.build_stubbed(:priority) }
@ -50,7 +46,8 @@ describe Bim::Bcf::API::V2_1::ProjectExtensions::Representer, 'rendering' do
model: work_package,
assignable_types: [type_task],
assignable_priorities: [priority],
assignable_statuses: [status])
assignable_statuses: [status],
assignable_assignees: [user])
end
let(:instance) { described_class.new(contract) }
let(:subject) { instance.to_json }

@ -268,7 +268,7 @@ class Budget < ApplicationRecord
attributes &&
attributes[:hours].to_f.positive? &&
attributes[:user_id].to_i.positive? &&
project.possible_assignees.map(&:id).include?(attributes[:user_id].to_i)
Principal.possible_assignee(project).where(id: attributes[:user_id].to_i).exists?
end
def valid_material_budget_attributes?(attributes)

@ -68,7 +68,7 @@ See docs/COPYRIGHT.rdoc for more details.
<td class="user">
<label class="hidden-for-sighted" for="<%= id_prefix %>_user_id"><%= t(:label_user) %></label>
<%= cost_form.select :user_id,
@project.possible_assignees.sort.map { |u| [u.name, u.id] },
Principal.possible_assignee(@project).sort.map { |u| [u.name, u.id] },
{ prompt: true },
{
index: id_or_index,

@ -38,8 +38,9 @@ module CostlogHelper
end
def user_collection_for_select_options(_options = {})
users = @project.possible_assignees
users.map { |t| [t.name, t.id] }
Principal
.possible_assignee(@project)
.map { |t| [t.name, t.id] }
end
def extended_progress_bar(pcts, options = {})

@ -43,6 +43,12 @@ describe 'Members widget on dashboard', type: :feature, js: true do
let!(:no_view_member_user) do
FactoryBot.create :user, lastname: "No_View", member_in_project: project, member_through_role: no_view_member_role
end
let!(:placeholder_user) do
FactoryBot.create :placeholder_user,
lastname: "Placeholder user",
member_in_project: project,
member_through_role: no_view_member_role
end
let!(:invisible_user) do
FactoryBot.create :user, lastname: "Invisible", member_in_project: other_project, member_through_role: role
end
@ -89,6 +95,8 @@ describe 'Members widget on dashboard', type: :feature, js: true do
.to have_content no_view_member_role
expect(page)
.to have_content no_view_member_user.name
expect(page)
.to have_content placeholder_user.name
end
end

@ -57,7 +57,7 @@ class CostQuery::Filter::UserId < Report::Filter::Base
# Excludes the anonymous user
users = User.joins(members: :project)
.merge(Project.visible)
.not_builtin
.human
.select(User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s) << :id)
.distinct

@ -55,6 +55,15 @@ describe WorkPackages::BaseContract do
end
let(:project) { FactoryBot.build_stubbed(:project) }
let(:current_user) { member }
let!(:assignable_assignees_scope) do
scope = double 'assignable assignees scope'
allow(Principal)
.to receive(:possible_assignee)
.and_return scope
scope
end
let(:permissions) do
%i(
view_work_packages

@ -31,7 +31,8 @@ require 'spec_helper'
require 'contracts/work_packages/shared_base_contract'
describe WorkPackages::CreateContract do
let(:work_package) { WorkPackage.new }
let(:work_package) { WorkPackage.new project: work_package_project }
let(:work_package_project) { project }
let(:project) { FactoryBot.build_stubbed(:project) }
let(:user) { FactoryBot.build_stubbed(:user) }

@ -29,7 +29,6 @@
#++
shared_examples_for 'work package contract' do
let(:project) { FactoryBot.build_stubbed(:project) }
let(:user) { FactoryBot.build_stubbed(:user) }
let(:other_user) { FactoryBot.build_stubbed(:user) }
let(:policy) { double(WorkPackagePolicy, allowed?: true) }
@ -48,6 +47,23 @@ shared_examples_for 'work package contract' do
.and_return(policy)
end
let(:possible_assignees) { [] }
let!(:assignable_assignees_scope) do
scope = double 'assignable assignees scope'
allow(Principal)
.to receive(:possible_assignee)
.with(work_package_project)
.and_return scope
allow(scope)
.to receive(:exists?) do |hash|
possible_assignees.map(&:id).include?(hash[:id])
end
scope
end
shared_examples_for 'has no error on' do |property|
it property do
expect(validated_contract.errors[property]).to be_empty
@ -55,37 +71,17 @@ shared_examples_for 'work package contract' do
end
describe 'assigned_to_id' do
let(:assignee_members) { double('assignee_members') }
before do
allow(work_package)
.to receive(:project)
.and_return(project)
allow(project)
.to receive(:possible_assignee_members)
.and_return(assignee_members)
allow(assignee_members)
.to receive(:exists?)
.with(user_id: other_user.id)
.and_return true
work_package.assigned_to = other_user
work_package.assigned_to_id = other_user.id
end
context 'if the assigned user is a possible assignee' do
let(:possible_assignees) { [other_user] }
it_behaves_like 'has no error on', :assigned_to
end
context 'if the assigned user is not a possible assignee' do
before do
allow(assignee_members)
.to receive(:exists?)
.with(user_id: other_user.id)
.and_return false
end
it 'is not a valid assignee' do
error = I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
property: I18n.t('attributes.assignee'))
@ -94,48 +90,23 @@ shared_examples_for 'work package contract' do
end
context 'if the project is not set' do
before do
allow(work_package)
.to receive(:project)
.and_return(nil)
end
let(:work_package_project) { nil }
it_behaves_like 'has no error on', :assigned_to
end
end
describe 'responsible_id' do
let(:responsible_members) { double('responsible_members') }
before do
allow(work_package)
.to receive(:project)
.and_return(project)
allow(project)
.to receive(:possible_responsible_members)
.and_return(responsible_members)
allow(responsible_members)
.to receive(:exists?)
.with(user_id: other_user.id)
.and_return true
work_package.responsible = other_user
work_package.responsible_id = other_user.id
end
context 'if the responsible user is a possible responsible' do
let(:possible_assignees) { [other_user] }
it_behaves_like 'has no error on', :responsible
end
context 'if the assigned user is not a possible responsible' do
before do
allow(responsible_members)
.to receive(:exists?)
.with(user_id: other_user.id)
.and_return false
end
it 'is not a valid responsible' do
error = I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
property: I18n.t('attributes.responsible'))
@ -144,13 +115,23 @@ shared_examples_for 'work package contract' do
end
context 'if the project is not set' do
before do
allow(work_package)
.to receive(:project)
.and_return(nil)
end
let(:work_package_project) { nil }
it_behaves_like 'has no error on', :responsible
end
end
describe '#assignable_assignees' do
it 'returns the Principal`s possible_assignee scope' do
expect(subject.assignable_assignees)
.to eql assignable_assignees_scope
end
end
describe '#assignable_responsibles' do
it 'returns the Principal`s possible_assignee scope' do
expect(subject.assignable_responsibles)
.to eql assignable_assignees_scope
end
end
end

@ -31,7 +31,7 @@ require 'spec_helper'
require 'contracts/work_packages/shared_base_contract'
describe WorkPackages::UpdateContract do
let(:project) do
let(:work_package_project) do
FactoryBot.build_stubbed(:project, public: false).tap do |p|
allow(Project)
.to receive(:find)
@ -41,7 +41,7 @@ describe WorkPackages::UpdateContract do
end
let(:work_package) do
FactoryBot.build_stubbed(:work_package,
project: project,
project: work_package_project,
type: type).tap do |wp|
wp_scope = double('wp scope')
@ -63,14 +63,17 @@ describe WorkPackages::UpdateContract do
before do
allow(user)
.to receive(:allowed_to?) do |permission, context|
permissions.include?(permission) && context == project
permissions.include?(permission) && context == work_package_project
end
end
subject(:contract) { described_class.new(work_package, user) }
it_behaves_like 'work package contract' do
let(:work_package) { FactoryBot.build_stubbed(:work_package) }
let(:work_package) do
FactoryBot.build_stubbed(:work_package,
project: work_package_project)
end
end
describe 'lock_version' do
@ -162,7 +165,7 @@ describe WorkPackages::UpdateContract do
before do
allow(user)
.to receive(:allowed_to?) do |permission, context|
permissions.include?(permission) && context == project ||
permissions.include?(permission) && context == work_package_project ||
target_permissions.include?(permission) && context == target_project
end
@ -171,7 +174,7 @@ describe WorkPackages::UpdateContract do
if work_package.project_id == target_project.id
target_project
else
project
work_package_project
end
end

@ -642,7 +642,7 @@ describe AccountController, type: :controller do
it 'set the user status to active' do
user = User.where(login: 'register').last
expect(user).not_to be_nil
expect(user.status).to eq(User::STATUSES[:active])
expect(user).to be_active
end
it 'calls the user_registered callback' do
@ -955,13 +955,13 @@ describe AccountController, type: :controller do
end
context 'registered user' do
let(:status) { User::STATUSES[:registered] }
let(:status) { User.statuses[:registered] }
it_behaves_like "activation is blocked due to user limit"
end
context 'invited user' do
let(:status) { User::STATUSES[:invited] }
let(:status) { User.statuses[:invited] }
it_behaves_like "activation is blocked due to user limit"
end

@ -111,7 +111,7 @@ describe MyController, type: :controller do
context 'when the user is invited' do
let!(:user) {
FactoryBot.create :user, login: login, status: Principal::STATUSES[:invited], auth_source_id: auth_source.id
FactoryBot.create :user, login: login, status: Principal.statuses[:invited], auth_source_id: auth_source.id
}
it "should log in given user and activate it" do

@ -333,7 +333,7 @@ describe UsersController, type: :controller do
describe '#change_status_info' do
let!(:registered_user) do
FactoryBot.create(:user, status: User::STATUSES[:registered])
FactoryBot.create(:user, status: User.statuses[:registered])
end
before do
@ -386,7 +386,7 @@ describe UsersController, type: :controller do
} do
describe 'WHEN activating a registered user' do
let!(:registered_user) do
FactoryBot.create(:user, status: User::STATUSES[:registered],
FactoryBot.create(:user, status: User.statuses[:registered],
language: 'de')
end
@ -399,7 +399,7 @@ describe UsersController, type: :controller do
post :change_status,
params: {
id: registered_user.id,
user: { status: User::STATUSES[:active] },
user: { status: User.statuses[:active] },
activate: '1'
}
end

@ -38,7 +38,7 @@ FactoryBot.define do
mail_notification { OpenProject::VERSION::MAJOR > 0 ? 'all' : true }
language { 'en' }
status { User::STATUSES[:active] }
status { User.statuses[:active] }
admin { false }
first_login { false if User.table_exists? and User.columns.map(&:name).include? 'first_login' }
@ -60,11 +60,11 @@ FactoryBot.define do
sequence(:mail) do |n| "bob#{n}.bobbit@bob.com" end
password { 'adminADMIN!' }
password_confirmation { 'adminADMIN!' }
status { User::STATUSES[:locked] }
status { User.statuses[:locked] }
end
factory :invited_user do
status { User::STATUSES[:invited] }
status { User.statuses[:invited] }
end
end

@ -26,7 +26,7 @@ describe "multi select custom values", js: true do
# We include an invited member to check at the same time that invited users are properly
# offered for user custom fields as they weren't before.
let(:member_statuses) do
[User::STATUSES[:active], User::STATUSES[:active], User::STATUSES[:invited]]
[User.statuses[:active], User.statuses[:active], User.statuses[:invited]]
end
let(:members) do

@ -34,21 +34,21 @@ feature 'group memberships through groups page', type: :feature, js: true do
let!(:peter) do
FactoryBot.create :user,
firstname: 'Peter',
lastname: 'Pan',
mail: 'foo@example.org',
member_in_project: project,
member_through_role: role
firstname: 'Peter',
lastname: 'Pan',
mail: 'foo@example.org',
member_in_project: project,
member_through_role: role
end
let!(:hannibal) do
FactoryBot.create :user,
firstname: 'Pan',
lastname: 'Hannibal',
mail: 'foo@example.com',
member_in_project: project,
member_through_role: role
firstname: 'Pan',
lastname: 'Hannibal',
mail: 'foo@example.com',
member_in_project: project,
member_through_role: role
end
let(:role) { FactoryBot.create(:role, permissions: %i(add_work_packages)) }

@ -28,97 +28,54 @@
require 'spec_helper'
feature 'group memberships through groups page', type: :feature, js: true do
feature 'Administrating memberships via the project settings', type: :feature, js: true do
using_shared_fixtures :admin
let!(:project) { FactoryBot.create :project, name: 'Project 1', identifier: 'project1' }
let(:current_user) do
FactoryBot.create(:user,
member_in_project: project,
member_through_role: manager)
end
let!(:project) { FactoryBot.create :project }
let!(:peter) { FactoryBot.create :user, firstname: 'Peter', lastname: 'Pan', mail: 'foo@example.org' }
let!(:hannibal) { FactoryBot.create :user, firstname: 'Hannibal', lastname: 'Smith', mail: 'boo@bar.org' }
let!(:crash) { FactoryBot.create :user, firstname: "<script>alert('h4x');</script>",
lastname: "<script>alert('h4x');</script>" }
let(:group) { FactoryBot.create :group, lastname: 'A-Team' }
let!(:manager) { FactoryBot.create :role, name: 'Manager' }
let!(:developer) { FactoryBot.create :role, name: 'Developer' }
let(:members_page) { Pages::Members.new project.identifier }
before do
allow(User).to receive(:current).and_return admin
group.add_members! peter
group.add_members! hannibal
let!(:developer_placeholder) { FactoryBot.create :placeholder_user, name: 'Developer 1' }
let!(:crash) do
FactoryBot.create :user,
firstname: "<script>alert('h4x');</script>",
lastname: "<script>alert('h4x');</script>"
end
shared_examples 'adding and removing principals' do
scenario 'Adding and Removing a Group as Member' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.add_user! 'A-Team', as: 'Manager'
expect(members_page).to have_added_group('A-Team')
SeleniumHubWaiter.wait
members_page.remove_group! 'A-Team'
expect(page).to have_text 'Removed A-Team from project'
expect(page).to have_text 'There are currently no members part of this project.'
end
scenario 'Adding and removing a User as Member' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.add_user! 'Hannibal Smith', as: 'Manager'
expect(members_page).to have_added_user 'Hannibal Smith'
SeleniumHubWaiter.wait
members_page.remove_user! 'Hannibal Smith'
expect(page).to have_text 'Removed Hannibal Smith from project'
expect(page).to have_text 'There are currently no members part of this project.'
let!(:group) do
FactoryBot.create(:group, lastname: 'A-Team').tap do |group|
User.execute_as User.admin.first do
group.add_members! peter
group.add_members! hannibal
end
end
end
scenario 'Entering a Username as Member in firstname, lastname order' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.open_new_member!
let!(:manager) { FactoryBot.create :role, name: 'Manager', permissions: [:manage_members] }
let!(:developer) { FactoryBot.create :role, name: 'Developer' }
let(:member1) { FactoryBot.create(:member, principal: peter, project: project, roles: [manager]) }
let(:member2) { FactoryBot.create(:member, principal: hannibal, project: project, roles: [developer]) }
let(:member3) { FactoryBot.create(:member, principal: group, project: project, roles: [manager]) }
SeleniumHubWaiter.wait
members_page.search_principal! 'Hannibal S'
expect(members_page).to have_search_result 'Hannibal Smith'
end
let!(:existing_members) { [] }
scenario 'Entering a Username as Member in lastname, firstname order' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.open_new_member!
let(:members_page) { Pages::Members.new project.identifier }
SeleniumHubWaiter.wait
members_page.search_principal! 'Smith, H'
expect(members_page).to have_search_result 'Hannibal Smith'
end
before do
login_as(admin)
scenario 'Escaping should work properly when entering a name' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.open_new_member!
SeleniumHubWaiter.wait
members_page.search_principal! 'script'
members_page.visit!
expect(members_page).not_to have_alert_dialog
expect(members_page).to have_search_result "<script>alert('h4x');</script>"
end
SeleniumHubWaiter.wait
end
context 'with members in the project' do
let!(:member1) { FactoryBot.create(:member, principal: peter, project: project, roles: [manager]) }
let!(:member2) { FactoryBot.create(:member, principal: hannibal, project: project, roles: [developer]) }
let!(:member3) { FactoryBot.create(:member, principal: group, project: project, roles: [manager]) }
let!(:existing_members) { [member1, member2, member3] }
scenario 'sorting the page' do
members_page.visit!
SeleniumHubWaiter.wait
members_page.sort_by 'last name'
members_page.expect_sorted_by 'last name'
@ -146,16 +103,62 @@ feature 'group memberships through groups page', type: :feature, js: true do
end
end
context 'with a user' do
it_behaves_like 'adding and removing principals'
scenario 'Adding and Removing a Group as Member' do
members_page.add_user! 'A-Team', as: 'Manager'
scenario 'Escaping should work properly when selecting a user' do
members_page.visit!
members_page.open_new_member!
members_page.select_principal! 'script'
expect(members_page).to have_added_group('A-Team')
SeleniumHubWaiter.wait
expect(members_page).not_to have_alert_dialog
expect(page).to have_text "<script>alert('h4x');</script>"
end
members_page.remove_group! 'A-Team'
expect(page).to have_text 'Removed A-Team from project'
expect(page).to have_text 'There are currently no members part of this project.'
end
scenario 'Adding and removing a User as Member' do
members_page.add_user! 'Hannibal Smith', as: 'Manager'
expect(members_page).to have_added_user 'Hannibal Smith'
SeleniumHubWaiter.wait
members_page.remove_user! 'Hannibal Smith'
expect(page).to have_text 'Removed Hannibal Smith from project'
expect(page).to have_text 'There are currently no members part of this project.'
end
scenario 'Adding and removing a Placeholder as Member' do
members_page.add_user! developer_placeholder.name, as: developer.name
expect(members_page).to have_added_user developer_placeholder.name
SeleniumHubWaiter.wait
members_page.remove_user! developer_placeholder.name
expect(page).to have_text "Removed #{developer_placeholder.name} from project"
expect(page).to have_text 'There are currently no members part of this project.'
end
scenario 'Entering a Username as Member in firstname, lastname order' do
members_page.open_new_member!
SeleniumHubWaiter.wait
members_page.search_principal! 'Hannibal S'
expect(members_page).to have_search_result 'Hannibal Smith'
end
scenario 'Entering a Username as Member in lastname, firstname order' do
members_page.open_new_member!
SeleniumHubWaiter.wait
members_page.search_principal! 'Smith, H'
expect(members_page).to have_search_result 'Hannibal Smith'
end
scenario 'Escaping should work properly when entering a name' do
members_page.open_new_member!
SeleniumHubWaiter.wait
members_page.search_principal! 'script'
expect(members_page).not_to have_alert_dialog
expect(members_page).to have_search_result "<script>alert('h4x');</script>"
end
end

@ -32,8 +32,8 @@ describe 'index users', type: :feature do
let!(:current_user) { FactoryBot.create :admin, created_at: 1.hour.ago }
let!(:anonymous) { FactoryBot.create :anonymous }
let!(:active_user) { FactoryBot.create :user, created_at: 1.minute.ago }
let!(:registered_user) { FactoryBot.create :user, status: User::STATUSES[:registered] }
let!(:invited_user) { FactoryBot.create :user, status: User::STATUSES[:invited] }
let!(:registered_user) { FactoryBot.create :user, status: User.statuses[:registered] }
let!(:invited_user) { FactoryBot.create :user, status: User.statuses[:invited] }
let(:index_page) { Pages::Admin::Users::Index.new }
before do

@ -96,7 +96,7 @@ describe 'user self registration', type: :feature, js: true do
expect(page)
.to have_content('Your account was created and is now pending administrator approval.')
registered_user = User.find_by(status: Principal::STATUSES[:registered])
registered_user = User.find_by(status: Principal.statuses[:registered])
# Trying unsuccessfully to login
login_with 'heidi', 'test123=321test'

@ -26,6 +26,11 @@ describe 'edit work package', js: true do
member_in_project: project,
member_through_role: manager_role
end
let(:placeholder_user) do
FactoryBot.create :placeholder_user,
member_in_project: project,
member_through_role: manager_role
end
let(:cf_all) do
FactoryBot.create :work_package_custom_field, is_for_all: true, field_format: 'text'
@ -176,6 +181,17 @@ describe 'edit work package', js: true do
expect(work_package.assigned_to).to be_nil
end
it 'allows selecting placeholder users for assignee and responsible' do
wp_page.update_attributes assignee: placeholder_user.name,
responsible: placeholder_user.name
wp_page.expect_attributes assignee: placeholder_user.name,
responsible: placeholder_user.name
wp_page.expect_activity_message("Assignee set to #{placeholder_user.name}")
wp_page.expect_activity_message("Accountable set to #{placeholder_user.name}")
end
context 'switching to custom field with required CF' do
let(:custom_field) do
FactoryBot.create(

@ -32,11 +32,13 @@ describe UsersHelper, type: :helper do
include UsersHelper
def build_user(status, blocked)
user = FactoryBot.build(:user)
allow(user).to receive(:status).and_return(User::STATUSES[status])
allow(user).to receive(:failed_too_many_recent_login_attempts?).and_return(blocked)
allow(user).to receive(:failed_login_count).and_return(3)
user
FactoryBot.build_stubbed(:user,
status: status,
failed_login_count: 3).tap do |user|
allow(user)
.to receive(:failed_too_many_recent_login_attempts?)
.and_return(blocked)
end
end
describe 'full_user_status' do

@ -102,37 +102,26 @@ describe ::API::V3::Groups::GroupRepresenter do
representer.to_json
end
describe 'caching' do
it 'is based on the representer\'s cache_key' do
expect(OpenProject::Cache)
.to receive(:fetch)
.with(representer.json_cache_key)
.and_call_original
representer.to_json
end
describe '#json_cache_key' do
let!(:former_cache_key) { representer.json_cache_key }
describe '#json_cache_key' do
let!(:former_cache_key) { representer.json_cache_key }
it 'includes the name of the representer class' do
expect(representer.json_cache_key)
.to include('API', 'V3', 'Groups', 'GroupRepresenter')
end
it 'includes the name of the representer class' do
it 'changes when the locale changes' do
I18n.with_locale(:fr) do
expect(representer.json_cache_key)
.to include('API', 'V3', 'Groups', 'GroupRepresenter')
end
it 'changes when the locale changes' do
I18n.with_locale(:fr) do
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
.not_to eql former_cache_key
end
end
it 'changes when the group is updated' do
group.updated_at = Time.now + 20.seconds
it 'changes when the group is updated' do
group.updated_at = Time.now + 20.seconds
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
end
end

@ -197,7 +197,7 @@ describe ::API::V3::Memberships::Schemas::MembershipSchemaRepresenter do
context 'if having no project' do
it_behaves_like 'links to allowed values via collection link' do
let(:href) do
statuses = [Principal::STATUSES[:locked].to_s]
statuses = [Principal.statuses[:locked].to_s]
filters = [{ 'status' => { 'operator' => '!', 'values' => statuses } }]
api_v3_paths.path_for(:principals, filters: filters)
@ -210,7 +210,7 @@ describe ::API::V3::Memberships::Schemas::MembershipSchemaRepresenter do
it_behaves_like 'links to allowed values via collection link' do
let(:href) do
statuses = [Principal::STATUSES[:locked].to_s]
statuses = [Principal.statuses[:locked].to_s]
status_filter = { 'status' => { 'operator' => '!', 'values' => statuses } }
member_filter = { 'member' => { 'operator' => '!', 'values' => [assigned_project.id.to_s] } }

@ -0,0 +1,173 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::PlaceholderUsers::PlaceholderUserRepresenter, 'rendering' do
include ::API::V3::Utilities::PathHelper
let(:placeholder_user) { FactoryBot.build_stubbed(:placeholder_user) }
let(:current_user) { FactoryBot.build_stubbed(:user) }
let(:representer) { described_class.new(placeholder_user, current_user: current_user) }
let(:memberships_path) do
filters = [
{
principal: {
operator: '=',
values: [placeholder_user.id.to_s]
}
}
]
api_v3_paths.path_for(:memberships, filters: filters)
end
let(:global_permissions) { [] }
subject(:generated) { representer.to_json }
before do
allow(current_user)
.to receive(:allowed_to_globally?) do |requested_permission|
global_permissions.include?(requested_permission)
end
end
describe '_links' do
context 'self' do
it_behaves_like 'has a titled link' do
let(:link) { 'self' }
let(:href) { api_v3_paths.placeholder_user placeholder_user.id }
let(:title) { placeholder_user.name }
end
end
context 'memberships' do
it_behaves_like 'has no link' do
let(:link) { 'memberships' }
end
context 'user allowed to see members' do
let(:global_permissions) { [:view_members] }
it_behaves_like 'has a titled link' do
let(:link) { 'memberships' }
let(:href) { memberships_path }
let(:title) { I18n.t(:label_member_plural) }
end
end
context 'user allowed to manage members' do
let(:global_permissions) { [:manage_members] }
it_behaves_like 'has a titled link' do
let(:link) { 'memberships' }
let(:href) { memberships_path }
let(:title) { I18n.t(:label_member_plural) }
end
end
end
end
describe 'properties' do
it_behaves_like 'property', :_type do
let(:value) { 'PlaceholderUser' }
end
context 'as regular user' do
it_behaves_like 'property', :id do
let(:value) { placeholder_user.id }
end
it_behaves_like 'property', :name do
let(:value) { placeholder_user.name }
end
it 'hides the updatedAt property' do
is_expected.not_to have_json_path('updatedAt')
end
it 'hides the createdAt property' do
is_expected.not_to have_json_path('createdAt')
end
end
context 'as admin' do
let(:current_user) { FactoryBot.build_stubbed(:admin) }
it_behaves_like 'property', :id do
let(:value) { placeholder_user.id }
end
it_behaves_like 'property', :name do
let(:value) { placeholder_user.name }
end
it_behaves_like 'datetime property', :createdAt do
let(:value) { placeholder_user.created_at }
end
it_behaves_like 'datetime property', :updatedAt do
let(:value) { placeholder_user.updated_at }
end
end
end
describe 'caching' do
it 'is based on the representer\'s cache_key' do
expect(OpenProject::Cache)
.to receive(:fetch)
.with(representer.json_cache_key)
.and_call_original
representer.to_json
end
describe '#json_cache_key' do
let!(:former_cache_key) { representer.json_cache_key }
it 'includes the name of the representer class' do
expect(representer.json_cache_key)
.to include('API', 'V3', 'PlaceholderUsers', 'PlaceholderUserRepresenter')
end
it 'changes when the locale changes' do
I18n.with_locale(:fr) do
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
end
it 'changes when the placeholder is updated' do
placeholder_user.updated_at = Time.now + 20.seconds
expect(representer.json_cache_key)
.not_to eql former_cache_key
end
end
end
end

@ -34,7 +34,7 @@ describe ::API::V3::Queries::QueryRepresenter do
let(:query) { FactoryBot.build_stubbed(:query, project: project) }
let(:unpersisted_query) { FactoryBot.build(:query, project: project, user: other_user) }
let(:project) { FactoryBot.build_stubbed(:project) }
let(:user) { double('current_user', allowed_to?: true, admin: true, admin?: true, active?: true) }
let(:user) { double('current_user', allowed_to_globally?: true, allowed_to?: true, admin: true, admin?: true, active?: true) }
let(:other_user) { FactoryBot.build_stubbed(:user) }
let(:embed_links) { true }
let(:representer) do

@ -29,7 +29,7 @@
require 'spec_helper'
describe ::API::V3::Users::UserRepresenter do
let(:status) { Principal::STATUSES[:active] }
let(:status) { Principal.statuses[:active] }
let(:user) { FactoryBot.build_stubbed(:user, status: status) }
let(:current_user) { FactoryBot.build_stubbed(:user) }
let(:representer) { described_class.new(user, current_user: current_user) }
@ -161,7 +161,7 @@ describe ::API::V3::Users::UserRepresenter do
end
context 'with a locked user' do
let(:status) { Principal::STATUSES[:locked] }
let(:status) { Principal.statuses[:locked] }
it_behaves_like 'has no link' do
let(:link) { 'showUser' }
@ -232,8 +232,8 @@ describe ::API::V3::Users::UserRepresenter do
describe 'memberships' do
before do
allow(current_user)
.to receive(:allowed_to?) do |action, _project, options|
permissions.include?(action) && options[:global]
.to receive(:allowed_to_globally?) do |action|
permissions.include?(action)
end
end

@ -223,7 +223,7 @@ describe ::API::V3::Utilities::CustomFieldInjector do
let(:path) { cf_path }
let(:href) do
params = [{ status: { operator: '!',
values: [Principal::STATUSES[:locked].to_s] } },
values: [Principal.statuses[:locked].to_s] } },
{ type: { operator: '=', values: ['User'] } },
{ member: { operator: '=', values: [schema.project_id.to_s] } }]

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save