Merge branch 'release/7.4' into dev

pull/6379/head
Jens Ulferts 7 years ago
commit 6c669aab6e
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 2
      Gemfile.lock
  2. 10
      app/assets/javascripts/members_form.js
  3. 4
      app/assets/stylesheets/content/_attributes_key_value.sass
  4. 2
      app/assets/stylesheets/content/_collapsible_section.sass
  5. 4
      app/assets/stylesheets/content/_widget_box.sass
  6. 1
      app/assets/stylesheets/layout/_drop_down.sass
  7. 5
      app/assets/stylesheets/openproject/_generic.sass
  8. 2
      app/assets/stylesheets/openproject/_index.sass
  9. 1
      app/assets/stylesheets/openproject/_scm.sass
  10. 11
      app/controllers/account_controller.rb
  11. 4
      app/controllers/activities_controller.rb
  12. 84
      app/controllers/concerns/user_limits.rb
  13. 6
      app/controllers/members_controller.rb
  14. 18
      app/controllers/users_controller.rb
  15. 14
      app/helpers/toolbar_helper.rb
  16. 9
      app/models/activity/work_package_activity_provider.rb
  17. 21
      app/services/work_packages/schedule_dependency.rb
  18. 8
      app/views/enterprises/_current.html.erb
  19. 7
      app/views/members/_member_form_non_impaired.html.erb
  20. 16
      app/views/users/index.html.erb
  21. 27
      docs/configuration/configuration.md
  22. 6
      docs/installation/manual/README.md
  23. 11
      extra/Apache/OpenProjectAuthentication.pm
  24. 34
      frontend/app/components/wp-edit-form/work-package-changeset.ts
  25. 4
      frontend/npm-shrinkwrap.json
  26. 2
      frontend/package.json
  27. 67
      lib/open_project/enterprise.rb
  28. 4
      lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb
  29. 60
      spec/controllers/account_controller_spec.rb
  30. 58
      spec/controllers/users_controller_spec.rb
  31. 148
      spec/features/work_packages/new/attributes_from_filter_spec.rb
  32. 80
      spec/lib/open_project/enterprise_spec.rb
  33. 73
      spec/views/users/index.html.erb_spec.rb

@ -558,7 +558,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
sprockets (3.7.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)

@ -48,6 +48,16 @@ function showAddMemberForm() {
jQuery('#filter-member-button').removeClass('-active');
window.OpenProject.guardedLocalStorage("showFilter", 'false');
jQuery('#add-member-button').prop('disabled', true);
jQuery("input#member_user_ids").on("change", function() {
var values = jQuery("input#member_user_ids").val();
if (values.indexOf("@") != -1) {
jQuery("#member-user-limit-warning").css("display", "block");
} else {
jQuery("#member-user-limit-warning").css("display", "none");
}
});
}
function hideAddMemberForm() {

@ -30,10 +30,6 @@
@include grid-block
@include grid-layout(2)
@include grid-visible-overflow
// Overriding position: relative defined by grid-block
// which seems to collide (chrome 66) with the column-count
// defined conditionally on the element as well.
position: static
font-size: 0.875rem
line-height: 1.6

@ -3,8 +3,8 @@
.collapsible-section--legend
&:before
@include icon-common
@include icon-mixin-arrow-down1
@include icon-common
font-size: 0.75rem
padding: 0.625rem 0.25rem 0 0.25rem

@ -99,8 +99,8 @@ $widget-box--enumeration-width: 20px
li:before
@include icon-font-common
@extend .icon-context
@include icon-mixin-arrow-right2
@extend .icon-context
display: inline-block
font-size: 0.6rem
color: $content-icon-link-color
@ -130,8 +130,8 @@ $widget-box--enumeration-width: 20px
li:before
@include icon-common
@extend .icon-yes
@include icon-mixin-yes
@extend .icon-yes
display: inline-block
font-size: 0.6rem
color: $alternative-color

@ -156,6 +156,7 @@
width: 100%
.drop-down .button--dropdown-indicator
@extend .icon-pulldown
&:before
@include icon-font-common
@include icon-mixin-pulldown

@ -0,0 +1,5 @@
.no-padding-bottom
padding-bottom: 0 !important
.display-inline
display: inline !important

@ -7,6 +7,8 @@
// Legacy styles, remove if possible
@import openproject/legacy
@import openproject/generic
@import openproject/mixins
@import openproject/onboarding
@import openproject/scm

@ -208,6 +208,7 @@ tr.dir
a
&.dir-expander i
@include icon-common
@extend .icon-plus
cursor: pointer
&:before
@include icon-mixin-plus

@ -34,6 +34,7 @@ class AccountController < ApplicationController
include Concerns::RedirectAfterLogin
include Concerns::AuthenticationStages
include Concerns::UserConsent
include Concerns::UserLimits
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_action :check_if_login_required
@ -180,6 +181,8 @@ class AccountController < ApplicationController
end
def activate_self_registered(token)
return if enforce_activation_user_limit
user = token.user
if not user.registered?
@ -211,6 +214,8 @@ class AccountController < ApplicationController
redirect_to home_url
else
return if enforce_activation_user_limit
activate_invited token
end
end
@ -570,6 +575,12 @@ class AccountController < ApplicationController
#
# Pass a block for behavior when a user fails to save
def register_automatically(user, opts = {})
if user_limit_reached?
show_user_limit_activation_error!
return redirect_back fallback_location: signin_path
end
# Automatic activation
user.activate

@ -36,10 +36,10 @@ class ActivitiesController < ApplicationController
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
begin; @date_to = params[:from].to_date + 1.day; rescue; end
end
@date_to ||= User.current.today + 1
@date_to ||= User.current.today + 1.day
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_work_packages? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))

@ -0,0 +1,84 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
##
# Intended to be used by the UsersController to enforce the user limit.
module Concerns::UserLimits
def enforce_user_limit(redirect_to: users_path, hard: OpenProject::Enterprise.fail_fast?)
if user_limit_reached?
if hard
show_user_limit_error!
redirect_back fallback_location: redirect_to
else
show_user_limit_warning!
end
true
else
false
end
end
def enforce_activation_user_limit(redirect_to: signin_path)
if user_limit_reached?
show_user_limit_activation_error!
redirect_back fallback_location: redirect_to
true
else
false
end
end
def show_user_limit_activation_error!
flash[:error] = I18n.t(:error_enterprise_activation_user_limit)
end
def show_user_limit_warning!
flash[:warning] = user_limit_warning
end
def show_user_limit_error!
flash[:error] = user_limit_warning
end
def user_limit_warning
warning = I18n.t(
:warning_user_limit_reached,
upgrade_url: OpenProject::Enterprise.upgrade_path
)
warning.html_safe
end
def user_limit_reached?
OpenProject::Enterprise.user_limit_reached?
end
end

@ -235,7 +235,7 @@ class MembersController < ApplicationController
user_ids.map do |id|
if id.to_i == 0 && id.present? # we've got an email - invite that user
# only admins can invite new users
if current_user.admin?
if current_user.admin? && enterprise_allow_new_users?
# The invitation can pretty much only fail due to the user already
# having been invited. So look them up if it does.
user = UserInvitation.invite_new_user(email: id) ||
@ -249,6 +249,10 @@ class MembersController < ApplicationController
end.compact
end
def enterprise_allow_new_users?
!OpenProject::Enterprise.user_limit_reached? || !OpenProject::Enterprise.fail_fast?
end
def each_comma_seperated(array, &block)
array.map { |e|
if e.to_s.match /\d(,\d)*/

@ -52,6 +52,9 @@ class UsersController < ApplicationController
include Concerns::PasswordConfirmation
before_action :check_password_confirmation, only: [:destroy]
include Concerns::UserLimits
before_action :enforce_user_limit, only: [:new, :create]
accept_key_auth :index, :show, :create, :update, :destroy
include SortHelper
@ -161,7 +164,13 @@ class UsersController < ApplicationController
if @user.invited?
# setting a password for an invited user activates them implicitly
@user.activate!
if OpenProject::Enterprise.user_limit_reached?
@user.register!
show_user_limit_warning!
else
@user.activate!
end
send_information = true
end
@ -204,6 +213,13 @@ class UsersController < ApplicationController
redirect_back_or_default(action: 'edit', id: @user)
return
end
if (params[:unlock] || params[:activate]) && user_limit_reached?
show_user_limit_error!
return redirect_back_or_default(action: 'edit', id: @user)
end
if params[:unlock]
@user.failed_login_count = 0
@user.activate

@ -2,11 +2,11 @@ module ToolbarHelper
include ERB::Util
include ActionView::Helpers::OutputSafetyHelper
def toolbar(title:, subtitle: '', link_to: nil, html: {})
def toolbar(title:, title_extra: nil, title_class: nil, subtitle: '', link_to: nil, html: {})
classes = ['toolbar-container', html[:class]].compact.join(' ')
content_tag :div, class: classes do
toolbar = content_tag :div, class: 'toolbar' do
dom_title(title, link_to) + dom_toolbar {
dom_title(title, link_to, title_class: title_class, title_extra: title_extra) + dom_toolbar {
yield if block_given?
}
end
@ -21,7 +21,7 @@ module ToolbarHelper
protected
def dom_title(raw_title, link_to = nil)
def dom_title(raw_title, link_to = nil, title_class: nil, title_extra: nil)
title = ''.html_safe
title << raw_title
@ -31,7 +31,13 @@ module ToolbarHelper
end
content_tag :div, class: 'title-container' do
content_tag(:h2, title)
opts = {}
opts[:class] = title_class if title_class.present?
content_tag(:h2, title, opts) + (
title_extra.present? ? title_extra : ''
)
end
end

@ -62,14 +62,7 @@ class Activity::WorkPackageActivityProvider < Activity::BaseActivityProvider
end
def event_type(event, _activity)
state = ''
journal = Journal.find(event['event_id'])
if journal.details.empty? && !journal.initial?
state = '-note'
else
state = ActiveRecord::Type::Boolean.new.cast(event['status_closed']) ? '-closed' : '-edit'
end
state = ActiveRecord::Type::Boolean.new.cast(event['status_closed']) ? '-closed' : '-edit'
"work_package#{state}"
end

@ -58,7 +58,9 @@ class WorkPackages::ScheduleDependency
attr_accessor :work_packages,
:dependencies,
:known_work_packages
:known_work_packages,
:known_work_packages_by_id,
:known_work_packages_by_parent_id
private
@ -69,8 +71,13 @@ class WorkPackages::ScheduleDependency
def load_all_following(work_packages)
following = load_following(work_packages)
# Those variables are pure optimizations.
# We want to reuse the already loaded work packages as much as possible
# and we want to have them readily available as hashes.
self.known_work_packages += following
known_work_packages.uniq!
self.known_work_packages_by_id = known_work_packages.group_by(&:id)
self.known_work_packages_by_parent_id = known_work_packages.group_by(&:parent_id)
new_dependencies = add_dependencies(following)
@ -184,23 +191,23 @@ class WorkPackages::ScheduleDependency
def ancestors_from_preloaded(work_package)
if work_package.parent_id
parent = known_work_packages.detect { |c| work_package.parent_id == c.id }
parent = known_work_packages_by_id[work_package.parent_id]
if parent
[parent] + ancestors_from_preloaded(parent)
parent + ancestors_from_preloaded(parent.first)
end
end || []
end
def descendants_from_preloaded(work_package)
children = known_work_packages.select { |c| c.parent_id == work_package.id }
children = known_work_packages_by_parent_id[work_package.id] || []
children + children.map { |child| descendants_from_preloaded(child) }.flatten
end
def known_work_packages
schedule_dependency.known_work_packages
end
delegate :known_work_packages,
:known_work_packages_by_id,
:known_work_packages_by_parent_id, to: :schedule_dependency
def scheduled_work_packages
schedule_dependency.work_packages + schedule_dependency.dependencies.keys

@ -13,6 +13,14 @@
<span><%= @current_token.mail %></span>
</div>
</div>
<% Hash(@current_token.restrictions).each do |key, value| %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name("#{key}_restriction") %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">
<span><%= value %></span>
</div>
</div>
<% end %>
<div class="attributes-key-value--key"><%= EnterpriseToken.human_attribute_name(:starts_at) %></div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value -text">

@ -81,4 +81,11 @@ See docs/COPYRIGHT.rdoc for more details.
</div>
</div>
</div>
<% if OpenProject::Enterprise.user_limit_reached? %>
<div class="notification-box -warning icon-warning" id="member-user-limit-warning" style="display: none;">
<div class="notification-box--content">
<p><%= I18n.t(:warning_user_limit_reached, upgrade_url: OpenProject::Enterprise.upgrade_path).html_safe %></p>
</div>
</div>
<% end %>
<% end %>

@ -26,8 +26,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% html_title l(:label_administration), l(:label_user_plural) -%>
<%= toolbar title: l(:label_user_plural) do %>
<% html_title t(:label_administration), t(:label_user_plural) -%>
<%
user_limit = begin
token = OpenProject::Enterprise.token
token && Hash(token.restrictions)[:active_user_count]
end
%>
<% users_info = user_limit && content_tag(:div) do %>
<%= t(:label_enterprise_active_users, current: User.active.count, limit: user_limit) %>
&nbsp;
<a href="<%= OpenProject::Enterprise.upgrade_path %>" class="display-inline button -tiny -highlight" title="<%= t(:title_enterprise_upgrade) %>"><%= t(:button_upgrade) %></a>
<% end %>
<%= toolbar title: t(:label_user_plural), title_class: 'no-padding-bottom', title_extra: users_info do %>
<li class="toolbar-item">
<%= link_to new_user_path,
{ class: 'button -alt-highlight',

@ -64,6 +64,7 @@ storage config above like this:
* [`blacklisted_routes`](#blacklisted-routes) (default: [])
* [`global_basic_auth`](#global-basic-auth)
* [`apiv3_enable_basic_auth`](#apiv3_enable_basic_auth)
* [`enterprise_limits`](#enterprise-limits)
## Setting session options
@ -338,3 +339,29 @@ default:
## Onboarding variables:
* `onboarding_video_url`: An URL for the video displayed on the onboarding modal. This is only shown when the user logs in for the first time.
### Enterprise Limits
If using an Enterprise token there are certain limits that apply.
You can configure how these limits are enforced.
#### `fail_fast`
*default: false*
If you set `fail_fast` to true, new users cannot be invited or registered if the user limit has been reached.
If it is false then you can still invite and register new users but their activation will fail until the
user limit has been increased (or the number of active users decreased).
Configured in the `configuration.yml` like this:
```
enterprise:
fail_fast: true
```
Or through the environment like this:
```
OPENPROJECT_ENTERPRISE_FAIL__FAST=true
```

@ -125,16 +125,16 @@ time to finsih.
[openproject@host] source ~/.profile
[openproject@host] git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
[openproject@host] rbenv install 2.3.0
[openproject@host] rbenv install 2.4.4
[openproject@host] rbenv rehash
[openproject@host] rbenv global 2.3.0
[openproject@host] rbenv global 2.4.4
```
To check our Ruby installation we run `ruby --version`. It should output
something very similar to:
```
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
ruby 2.4.4p296 (2018-03-28 revision 63013) [x86_64-linux]
```
## Installation of Node

@ -110,7 +110,12 @@ sub is_access_allowed {
my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );
my $key = $cfg->{OpenProjectApiKey};
my $openproject_url = $cfg->{OpenProjectUrl} . '/sys/repo_auth';
# Trim url base if users add trailing slash
my $url_base = $cfg->{OpenProjectUrl};
$url_base =~ s|/$||;
my $openproject_url = "$url_base/sys/repo_auth";
my $openproject_unparsed_uri = $r->unparsed_uri;
my $openproject_location = $r->location;
my $openproject_git_smart_http = 0;
@ -130,6 +135,10 @@ sub is_access_allowed {
my $ua = LWP::UserAgent->new;
my $response = $ua->request($openproject_req);
unless($response->is_success()) {
$r->log_error("Failed authorization for $login on $openproject_url: " . $response->status_line);
}
return $response->is_success();
}

@ -134,7 +134,7 @@ export class WorkPackageChangeset {
if (this.wpForm.hasValue()) {
return Promise.resolve(this.wpForm.value!);
} else {
return new Promise<FormResource>((resolve) => this.wpForm.valuesPromise().then(resolve));
return new Promise<FormResource>((resolve,) => this.wpForm.valuesPromise().then(resolve));
}
}
@ -279,20 +279,30 @@ export class WorkPackageChangeset {
* Extract the link(s) in the given changed value
*/
private getLinkedValue(val:any, fieldSchema:op.FieldSchema) {
var isArray = (fieldSchema.type || '').startsWith('[]');
// Links should always be nullified as { href: null }, but
// this wasn't always the case, so ensure null values are returned as such.
if (_.isNil(val)) {
return {href: null};
}
if (isArray) {
var links:{ href:string }[] = [];
// Test if we either have a CollectionResource or a HAL array,
// or a single hal value.
let isArrayType = (fieldSchema.type || '').startsWith('[]');
let isArray = false;
if (val) {
var elements = (val.forEach && val) || val.elements;
if (val.forEach || val.elements) {
isArray = true;
}
elements.forEach((link:{ href:string }) => {
if (link.href) {
links.push({href: link.href});
}
});
}
if (isArray && isArrayType) {
var links:{ href:string }[] = [];
var elements = val.forEach ? val : val.elements;
elements.forEach((link:{ href:string }) => {
if (link.href) {
links.push({href: link.href});
}
});
return links;
} else {

@ -2145,9 +2145,7 @@
"dev": true
},
"foundation-apps": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/foundation-apps/-/foundation-apps-1.1.0.tgz",
"integrity": "sha1-urkXW+qLXSN8qH/j7MIUqtM9z5w="
"version": "git://github.com/opf/foundation-apps.git#1bc8c3ab51e757d19a636747e45e519cde00e902"
},
"fresh": {
"version": "0.5.2",

@ -88,7 +88,7 @@
"extract-text-webpack-plugin": "3.0.0",
"file-loader": "^0.8.1",
"fork-ts-checker-webpack-plugin": "^0.2.8",
"foundation-apps": "1.1.0",
"foundation-apps": "git://github.com/opf/foundation-apps.git#1bc8c3ab51e757d19a636747e45e519cde00e902",
"fuse.js": "^3.2.0",
"glob": "^4.5.3",
"happypack": "^4.0.0",

@ -0,0 +1,67 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject
module Enterprise
class << self
def token
EnterpriseToken.current.presence
end
def upgrade_path
url_helpers.enterprise_path
end
def user_limit
Hash(token.restrictions)[:active_user_count] if token
end
def active_user_count
User.active.count
end
##
# Indicates if there are more active users than the support token allows for.
#
# @return [Boolean] True if and only if there is a support token the user limit of which is exceeded.
def user_limit_reached?
active_user_count >= user_limit if user_limit
end
def fail_fast?
Hash(OpenProject::Configuration["enterprise"])["fail_fast"]
end
private
def url_helpers
@url_helpers ||= OpenProject::StaticRouting::StaticUrlHelpers.new
end
end
end
end

@ -140,8 +140,7 @@ module Redmine
end
def join_with_projects_table(query, project_ref_table)
query = query.join(projects_table).on(projects_table[:id].eq(project_ref_table['project_id']))
query
query.join(projects_table).on(projects_table[:id].eq(project_ref_table['project_id']))
end
def restrict_projects_by_selection(options, query)
@ -177,7 +176,6 @@ module Redmine
stmt = nil
perm = Redmine::AccessControl.permission(options[:permission])
is_member = options[:member]
original_query = query.dup
if user.logged?
allowed_projects = []

@ -445,6 +445,30 @@ describe AccountController, type: :controller do
expect(user.status).to eq(User::STATUSES[:active])
end
end
context "with user limit reached" do
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
end
it "fails" do
post :register,
params: {
user: {
login: 'register',
password: 'adminADMIN!',
password_confirmation: 'adminADMIN!',
firstname: 'John',
lastname: 'Doe',
mail: 'register@example.com'
}
}
is_expected.to redirect_to(signin_path)
expect(flash[:error]).to match /user limit reached/
end
end
end
context 'with password login disabled' do
@ -642,6 +666,42 @@ describe AccountController, type: :controller do
end
end
context 'POST activate' do
let(:user) { FactoryGirl.create :user, status: status }
let(:status) { -1 }
let(:token) { Token::Invitation.create!(user_id: user.id) }
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
post :activate, params: { token: token.value }
end
shared_examples "activation is blocked due to user limit" do
it "does not activate the user" do
expect(user.reload).not_to be_active
end
it "redirects back to the login page and shows the user limit error" do
expect(response).to redirect_to(signin_path)
expect(flash[:error]).to match /user limit reached.*contact.*admin/i
end
end
context 'registered user' do
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] }
it_behaves_like "activation is blocked due to user limit"
end
end
describe 'GET #auth_source_sso_failed (/sso)' do
render_views

@ -49,6 +49,48 @@ describe UsersController, type: :controller do
let(:admin) { FactoryBot.create(:admin) }
let(:anonymous) { FactoryBot.create(:anonymous) }
describe 'GET new' do
context "with user limit reached" do
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(true)
end
context "with fail fast" do
before do
allow(OpenProject::Enterprise).to receive(:fail_fast?).and_return(true)
as_logged_in_user admin do
get :new
end
end
it "shows a user limit error" do
expect(flash[:error]).to match /user limit reached/i
end
it "redirects back to the user index" do
expect(response).to redirect_to users_path
end
end
context "without fail fast" do
before do
as_logged_in_user admin do
get :new
end
end
it "shows a user limit warning" do
expect(flash[:warning]).to match /user limit reached/i
end
it "shows the new user page" do
expect(response).to render_template("users/new")
end
end
end
end
describe 'GET deletion_info' do
describe "WHEN the current user is the requested user
WHEN the setting users_deletable_by_self is set to true" do
@ -364,7 +406,11 @@ describe UsersController, type: :controller do
language: 'de')
end
let(:user_limit_reached) { false }
before do
allow(OpenProject::Enterprise).to receive(:user_limit_reached?).and_return(user_limit_reached)
as_logged_in_user admin do
post :change_status,
params: {
@ -388,6 +434,18 @@ describe UsersController, type: :controller do
locale: 'de'))
end
end
context "with user limit reached" do
let(:user_limit_reached) { true }
it "shows the user limit reached error and recommends to upgrade" do
expect(flash[:error]).to match /user limit reached.*upgrade/i
end
it "does not activate the user" do
expect(registered_user.reload).not_to be_active
end
end
end
end

@ -44,13 +44,6 @@ RSpec.feature 'Work package create uses attributes from filters', js: true, sele
let(:filters) { ::Components::WorkPackages::Filters.new }
let(:role) { FactoryBot.create :existing_role, permissions: [:view_work_packages] }
let!(:assignee) do
FactoryBot.create(:user,
firstname: 'An',
lastname: 'assignee',
member_in_project: project,
member_through_role: role)
end
let!(:query) do
FactoryBot.build(:query, project: project, user: user).tap do |query|
@ -68,63 +61,120 @@ RSpec.feature 'Work package create uses attributes from filters', js: true, sele
filters.expect_filter_count 0
filters.open
filters.add_filter_by('Assignee', 'is', assignee.name)
filters.add_filter_by('Type', 'is', type_task.name)
end
it 'uses the assignee filter in inline-create and split view' do
wp_table.click_inline_create
context 'with a multi-value custom field' do
let(:type_task) { FactoryGirl.create(:type_task, custom_fields: [custom_field]) }
let!(:project) {
FactoryGirl.create :project,
types: [type_task],
work_package_custom_fields: [custom_field]
}
let!(:custom_field) do
FactoryGirl.create(
:list_wp_custom_field,
multi_value: true,
is_filter: true,
name: "Gate",
possible_values: %w(A B C),
is_required: false
)
end
subject_field = wp_table.edit_field(nil, :subject)
subject_field.expect_active!
before do
filters.add_filter_by('Gate', 'is', 'A', "customField#{custom_field.id}")
end
# Expect assignee to be set to the current user
assignee_field = wp_table.edit_field(nil, :assignee)
assignee_field.expect_state_text assignee.name
it 'allows to save with a single value (Regression test #27833)' do
split_page = wp_table.create_wp_split_screen type_task.name
subject = split_page.edit_field(:subject)
subject.expect_active!
subject.set_value 'Foobar!'
subject.submit_by_enter
wp_table.expect_notification(
message: 'Successful creation. Click here to open this work package in fullscreen view.'
)
wp_table.dismiss_notification!
wp = WorkPackage.last
expect(wp.subject).to eq 'Foobar!'
expect(wp.send("custom_field_#{custom_field.id}")).to eq %w(A)
expect(wp.type_id).to eq type_task.id
end
end
# Expect type set to task
assignee_field = wp_table.edit_field(nil, :type)
assignee_field.expect_state_text type_task.name
context 'with assignee filter' do
let!(:assignee) do
FactoryGirl.create(:user,
firstname: 'An',
lastname: 'assignee',
member_in_project: project,
member_through_role: role)
end
# Save the WP
subject_field.set_value 'Foobar!'
subject_field.submit_by_enter
wp_table.expect_notification(
message: 'Successful creation. Click here to open this work package in fullscreen view.'
)
wp_table.dismiss_notification!
before do
filters.add_filter_by('Assignee', 'is', assignee.name)
end
wp = WorkPackage.last
expect(wp.subject).to eq 'Foobar!'
expect(wp.assigned_to_id).to eq assignee.id
expect(wp.type_id).to eq type_task.id
it 'uses the assignee filter in inline-create and split view' do
wp_table.click_inline_create
# Open split view create
split_view_create.click_create_wp_button(type_bug)
subject_field = wp_table.edit_field(nil, :subject)
subject_field.expect_active!
# Subject
subject_field = split_view_create.edit_field :subject
subject_field.expect_active!
subject_field.set_value 'Split Foobar!'
# Expect assignee to be set to the current user
assignee_field = wp_table.edit_field(nil, :assignee)
assignee_field.expect_state_text assignee.name
# Type field IS NOT synced
type_field = split_view_create.edit_field :type
type_field.expect_state_text type_bug
# Expect type set to task
assignee_field = wp_table.edit_field(nil, :type)
assignee_field.expect_state_text type_task.name
# Assignee is synced
assignee_field = split_view_create.edit_field :assignee
assignee_field.expect_value "/api/v3/users/#{assignee.id}"
# Save the WP
subject_field.set_value 'Foobar!'
subject_field.submit_by_enter
within '.work-packages--edit-actions' do
click_button 'Save'
end
wp_table.expect_notification(
message: 'Successful creation. Click here to open this work package in fullscreen view.'
)
wp_table.dismiss_notification!
wp_table.expect_notification(message: 'Successful creation.')
wp = WorkPackage.last
expect(wp.subject).to eq 'Foobar!'
expect(wp.assigned_to_id).to eq assignee.id
expect(wp.type_id).to eq type_task.id
wp = WorkPackage.last
expect(wp.subject).to eq 'Split Foobar!'
expect(wp.assigned_to_id).to eq assignee.id
expect(wp.type_id).to eq type_bug.id
# Open split view create
split_view_create.click_create_wp_button(type_bug)
# Subject
subject_field = split_view_create.edit_field :subject
subject_field.expect_active!
subject_field.set_value 'Split Foobar!'
# Type field IS NOT synced
type_field = split_view_create.edit_field :type
type_field.expect_state_text type_bug
# Assignee is synced
assignee_field = split_view_create.edit_field :assignee
assignee_field.expect_value "/api/v3/users/#{assignee.id}"
within '.work-packages--edit-actions' do
click_button 'Save'
end
wp_table.expect_notification(message: 'Successful creation.')
wp = WorkPackage.last
expect(wp.subject).to eq 'Split Foobar!'
expect(wp.assigned_to_id).to eq assignee.id
expect(wp.type_id).to eq type_bug.id
end
end
end

@ -0,0 +1,80 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'open_project/passwords'
describe OpenProject::Enterprise do
describe "#user_limit_reached?" do
let(:user_limit) { 2 }
before do
allow(OpenProject::Enterprise).to receive(:user_limit).and_return(user_limit)
end
context "with fewer active users than the limit allows" do
before do
FactoryGirl.create :user
expect(User.active.count).to eq 1
end
it "is false" do
expect(subject).not_to be_user_limit_reached
end
end
context "with equal or more active users than the limit allows" do
shared_examples "user limit is reached" do
let(:num_active_users) { 0 }
before do
(1..num_active_users).each { |_i| FactoryGirl.create :user }
expect(User.active.count).to eq num_active_users
end
it "is true" do
expect(subject).to be_user_limit_reached
end
end
context "(equal)" do
it_behaves_like "user limit is reached" do
let(:num_active_users) { user_limit }
end
end
context "(more)" do
it_behaves_like "user limit is reached" do
let(:num_active_users) { user_limit + 1 }
end
end
end
end
end

@ -0,0 +1,73 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This 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-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'users/index', type: :view do
let!(:admin) { FactoryGirl.create :admin }
let!(:user) { FactoryGirl.create :user, firstname: "Scarlet", lastname: "Scallywag" }
before do
assign(:users, [admin, user])
assign(:status, "all")
assign(:groups, Group.all)
allow(view).to receive(:current_user).and_return(admin)
allow_any_instance_of(TableCell).to receive(:controller_name).and_return("users")
allow_any_instance_of(TableCell).to receive(:action_name).and_return("index")
end
it 'renders the user table' do
render
expect(rendered).to have_text(admin.name)
expect(rendered).to have_text("Scarlet Scallywag")
end
context "with an Enterprise token" do
before do
allow(OpenProject::Enterprise).to receive(:token).and_return(Struct.new(:restrictions).new({active_user_count: 5}))
end
it "shows the current number of active and allowed users" do
render
# expected active users: admin and user from above
expect(rendered).to have_text("2/5 booked active users")
end
end
context "without an Enterprise token" do
it "does not show the current number of active and allowed users" do
render
expect(rendered).not_to have_text("booked active users")
end
end
end
Loading…
Cancel
Save