Merge pull request #3480 from machisuji/feature/invitations

Feature: invitations
pull/3501/head
Oliver Günther 9 years ago
commit d7b5284c66
  1. 11
      app/assets/javascripts/admin_users.js
  2. 7
      app/assets/javascripts/members_select_boxes.js
  3. 8
      app/assets/stylesheets/content/_accounts.sass
  4. 170
      app/controllers/account_controller.rb
  5. 7
      app/controllers/auth_sources_controller.rb
  6. 17
      app/controllers/concerns/omniauth_login.rb
  7. 108
      app/controllers/concerns/user_invitation.rb
  8. 109
      app/controllers/invitations_controller.rb
  9. 189
      app/controllers/members_controller.rb
  10. 44
      app/controllers/users_controller.rb
  11. 7
      app/helpers/projects_helper.rb
  12. 39
      app/models/dummy_auth_source.rb
  13. 10
      app/models/member.rb
  14. 7
      app/models/principal.rb
  15. 3
      app/models/project.rb
  16. 45
      app/models/user.rb
  17. 5
      app/views/account/_auth_providers.html.erb
  18. 4
      app/views/account/_password_login_form.html.erb
  19. 2
      app/views/account/register.html.erb
  20. 16
      app/views/members/_member_form_impaired.html.erb
  21. 21
      app/views/members/_member_form_non_impaired.html.erb
  22. 25
      app/views/members/autocomplete_for_member.json.erb
  23. 4
      app/views/members/create.js.erb
  24. 155
      app/views/members/index.html.erb
  25. 43
      app/views/members/new.html.erb
  26. 147
      app/views/projects/settings/_members.html.erb
  27. 60
      app/views/users/_simple_form.html.erb
  28. 7
      app/views/users/new.html.erb
  29. 6
      config/initializers/menus.rb
  30. 7
      config/initializers/permissions.rb
  31. 6
      config/initializers/user_invitation.rb
  32. 35
      config/locales/en.yml
  33. 9
      config/routes.rb
  34. 3
      db/seeds/roles.rb
  35. 186
      features/groups/group_memberships.feature
  36. 99
      features/groups/membership.feature
  37. 114
      features/members/error_messages.feature
  38. 154
      features/members/membership.feature
  39. 79
      features/members/pagination.feature
  40. 49
      features/projects/settings.feature
  41. 9
      features/users/add_user.feature
  42. 12
      features/users/random_password_assignment.feature
  43. 26
      lib/open_project/notifications.rb
  44. 2
      lib/open_project/object_linking.rb
  45. 60
      lib/tasks/ldap.rake
  46. 67
      spec/controllers/members_controller_spec.rb
  47. 4
      spec/factories/auth_source_factory.rb
  48. 96
      spec/features/groups/group_memberships_spec.rb
  49. 30
      spec/features/groups/groups_spec.rb
  50. 126
      spec/features/groups/membership_spec.rb
  51. 73
      spec/features/members/error_messages_spec.rb
  52. 127
      spec/features/members/membership_spec.rb
  53. 84
      spec/features/members/pagination_spec.rb
  54. 71
      spec/features/members/roles_spec.rb
  55. 67
      spec/features/users/create_spec.rb
  56. 12
      spec/legacy/functional/users_controller_spec.rb
  57. 40
      spec/lib/open_project/notifications_spec.rb
  58. 128
      spec/support/pages/groups.rb
  59. 143
      spec/support/pages/members.rb
  60. 80
      spec/support/pages/page.rb

@ -44,14 +44,21 @@
// Hide password fields when non-internal authentication source is selected // Hide password fields when non-internal authentication source is selected
function on_auth_source_change() { function on_auth_source_change() {
var passwordFields = jQuery('#password_fields'), var passwordFields = jQuery('#password_fields'),
passwordInputs = passwordFields.find('#user_password, #user_password_confirmation'); passwordInputs = passwordFields.find('#user_password, #user_password_confirmation'),
newUserLogin = jQuery('#new_user_login');
if (this.value === '') { if (this.value === '') {
passwordFields.show(); passwordFields.show();
passwordInputs.removeAttr('disabled'); passwordInputs.removeProp('disabled');
newUserLogin.hide();
newUserLogin.find('input').prop('disabled', true);
} else { } else {
passwordFields.hide(); passwordFields.hide();
passwordInputs.prop('disabled', 'disabled'); passwordInputs.prop('disabled', 'disabled');
newUserLogin.show();
newUserLogin.find('input').prop('disabled', false);
} }
} }

@ -103,10 +103,5 @@ jQuery(document).ready(function($) {
}); });
}; };
memberstab = $('#tab-members').first(); init_members_cb();
if ((memberstab !== null) && (memberstab.hasClass("selected"))) {
init_members_cb();
} else {
memberstab.click(init_members_cb);
}
}); });

@ -38,6 +38,14 @@
float: right float: right
text-align: right text-align: right
#content .login-auth-providers.wide
width: auto
text-align: center
a.auth-provider
float: none
display: inline-block
// use id selectors to be specific enough to override general content and top-menu definitions // use id selectors to be specific enough to override general content and top-menu definitions
#content .login-auth-providers, #top-menu #nav-login-content .login-auth-providers #content .login-auth-providers, #top-menu #nav-login-content .login-auth-providers
width: 471px width: 471px

@ -113,28 +113,12 @@ class AccountController < ApplicationController
def register def register
return self_registration_disabled unless allow_registration? return self_registration_disabled unless allow_registration?
@user = invited_user
if request.get? if request.get?
session[:auth_source_registration] = nil registration_through_invitation!
@user = User.new(language: Setting.default_language)
else else
@user = User.new self_registration!
@user.admin = false
@user.register
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
end
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login]
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting @user
end
end end
end end
@ -142,7 +126,7 @@ class AccountController < ApplicationController
allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login? allow = Setting.self_registration? && !OpenProject::Configuration.disable_password_login?
get = request.get? && allow get = request.get? && allow
post = request.post? && (session[:auth_source_registration] || allow) post = (request.post? || request.patch?) && (session[:auth_source_registration] || allow)
get || post get || post
end end
@ -153,17 +137,82 @@ class AccountController < ApplicationController
# Token based account activation # Token based account activation
def activate def activate
return redirect_to(home_url) unless Setting.self_registration? && params[:token] token = Token.find_by value: params[:token].to_s
token = Token.find_by(action: 'register', value: params[:token].to_s)
redirect_to(home_url) && return unless token and !token.expired? if token && token.action == 'register' && Setting.self_registration?
activate_self_registered token
else
activate_invited token
end
end
def activate_self_registered(token)
user = token.user user = token.user
redirect_to(home_url) && return unless user.registered?
user.activate if not user.registered?
if user.save if user.active?
token.destroy flash[:notice] = I18n.t(:notice_account_already_activated)
flash[:notice] = l(:notice_account_activated) else
flash[:error] = I18n.t(:notice_activation_failed)
end
redirect_to home_url
else
user.activate
if user.save
token.destroy
flash[:notice] = I18n.t(:notice_account_activated)
else
flash[:error] = I18n.t(:notice_activation_failed)
end
redirect_to signin_path
end
end
def activate_by_token(token)
if token.nil? || token.expired? || !token.user.invited?
flash[:error] = I18n.t(:notice_account_invalid_token)
redirect_to home_url
else
activate_invited token
end end
redirect_to action: 'login' end
def activate_invited(token)
session[:invitation_token] = token.value
user = token.user
if user.auth_source
activate_through_auth_source user
else
activate_user user
end
end
def activate_user(user)
if Concerns::OmniauthLogin.direct_login?
direct_login user
elsif OpenProject::Configuration.disable_password_login?
flash[:notice] = I18n.t('account.omniauth_login')
redirect_to signin_path
else
redirect_to account_register_path
end
end
def activate_through_auth_source(user)
session[:auth_source_registration] = {
login: user.login,
auth_source_id: user.auth_source_id
}
flash[:notice] = I18n.t('account.auth_source_login', login: user.login).html_safe
redirect_to signin_path(username: user.login)
end end
# Process a password change form, used when the user is forced # Process a password change form, used when the user is forced
@ -199,6 +248,46 @@ class AccountController < ApplicationController
private private
def registration_through_invitation!
session[:auth_source_registration] = nil
if @user.nil?
@user = User.new(language: Setting.default_language)
elsif user_with_placeholder_name?(@user)
# force user to give their name
@user.firstname = nil
@user.lastname = nil
end
end
def self_registration!
if @user.nil?
@user = User.new
@user.admin = false
@user.register
end
if session[:auth_source_registration]
# on-the-fly registration via omniauth or via auth source
if pending_omniauth_registration?
register_via_omniauth(@user, session, permitted_params)
else
register_and_login_via_authsource(@user, session, permitted_params)
end
else
@user.attributes = permitted_params.user
@user.login = params[:user][:login]
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
register_user_according_to_setting @user
end
end
def user_with_placeholder_name?(user)
user.firstname == user.login and user.login == user.mail
end
def direct_login(user) def direct_login(user)
if flash.empty? if flash.empty?
ps = {}.tap do |p| ps = {}.tap do |p|
@ -233,7 +322,7 @@ class AccountController < ApplicationController
end end
def password_authentication(username, password) def password_authentication(username, password)
user = User.try_to_login(username, password) user = User.try_to_login(username, password, session)
if user.nil? if user.nil?
# login failed, now try to find out why and do the appropriate thing # login failed, now try to find out why and do the appropriate thing
user = User.find_by_login(username) user = User.find_by_login(username)
@ -251,6 +340,8 @@ class AccountController < ApplicationController
else else
invalid_credentials invalid_credentials
end end
elsif user and user.invited?
invited_account_not_activated(user)
else else
# incorrect password # incorrect password
invalid_credentials invalid_credentials
@ -346,6 +437,8 @@ class AccountController < ApplicationController
# Register a user depending on Setting.self_registration # Register a user depending on Setting.self_registration
def register_user_according_to_setting(user, opts = {}, &block) def register_user_according_to_setting(user, opts = {}, &block)
return register_automatically(user, opts, &block) if user.invited?
case Setting.self_registration case Setting.self_registration
when '1' when '1'
register_by_email_activation(user, opts, &block) register_by_email_activation(user, opts, &block)
@ -433,6 +526,13 @@ class AccountController < ApplicationController
end end
end end
def invited_account_not_activated(user)
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip}" \
" at #{Time.now.utc} (invited, NOT ACTIVATED)"
flash[:error] = I18n.t('account.error_inactive_activation_by_mail')
end
# Log an attempt to log in to a locked account or with invalid credentials # Log an attempt to log in to a locked account or with invalid credentials
# and show a flash message. # and show a flash message.
def invalid_credentials(flash_now: true) def invalid_credentials(flash_now: true)
@ -464,4 +564,12 @@ class AccountController < ApplicationController
redirect_back_or_default controller: '/my', action: 'page' redirect_back_or_default controller: '/my', action: 'page'
end end
end end
def invited_user
if session.include? :invitation_token
token = Token.find_by(value: session[:invitation_token])
token.user
end
end
end end

@ -84,9 +84,12 @@ class AuthSourcesController < ApplicationController
def destroy def destroy
@auth_source = AuthSource.find(params[:id]) @auth_source = AuthSource.find(params[:id])
unless @auth_source.users.first if @auth_source.users.empty?
@auth_source.destroy @auth_source.destroy
flash[:notice] = l(:notice_successful_delete)
flash[:notice] = t(:notice_successful_delete)
else
flash[:warning] = t(:notice_wont_delete_auth_source)
end end
redirect_to action: 'index' redirect_to action: 'index'
end end

@ -43,7 +43,18 @@ module Concerns::OmniauthLogin
# Set back url to page the omniauth login link was clicked on # Set back url to page the omniauth login link was clicked on
params[:back_url] = request.env['omniauth.origin'] params[:back_url] = request.env['omniauth.origin']
user = User.find_or_initialize_by identity_url: identity_url_from_omniauth(auth_hash)
user =
if session.include? :invitation_token
tok = Token.find_by value: session[:invitation_token]
u = tok.user
u.identity_url = identity_url_from_omniauth(auth_hash)
tok.destroy
session.delete :invitation_token
u
else
User.find_or_initialize_by identity_url: identity_url_from_omniauth(auth_hash)
end
decision = OpenProject::OmniAuth::Authorization.authorized? auth_hash decision = OpenProject::OmniAuth::Authorization.authorized? auth_hash
if decision.approve? if decision.approve?
@ -83,7 +94,7 @@ module Concerns::OmniauthLogin
private private
def authorization_successful(user, auth_hash) def authorization_successful(user, auth_hash)
if user.new_record? if user.new_record? || user.invited?
create_user_from_omniauth user, auth_hash create_user_from_omniauth user, auth_hash
else else
if user.active? if user.active?
@ -144,7 +155,7 @@ module Concerns::OmniauthLogin
def fill_user_fields_from_omniauth(user, auth) def fill_user_fields_from_omniauth(user, auth)
user.update_attributes omniauth_hash_to_user_attributes(auth) user.update_attributes omniauth_hash_to_user_attributes(auth)
user.register user.register unless user.invited?
user user
end end

@ -0,0 +1,108 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
module UserInvitation
EVENT_NAME = 'user_invited'
module_function
##
# Creates an invited user with the given email address.
# If no first and last is given it will default to 'OpenProject User'
# for the first name and 'To-be' for the last name.
# The default login is the email address.
#
# @param email E-Mail address the invitation is sent to.
# @param login User's login (optional)
# @param first_name The user's first name (optional)
# @param last_name The user's last name (optional)
#
# @yield [user] Allows modifying the created user before saving it.
#
# @return The invited user. If the invitation failed, calling `#registered?`
# on the returned user will yield `false`. Check for validation errors
# in that case.
def invite_new_user(email:, login: nil, first_name: nil, last_name: nil)
user = User.new login: login || email,
mail: email,
firstname: first_name || email,
lastname: last_name || '(invited)'
yield user if block_given?
invite_user! user
end
##
# Invites the given user. An email will be sent to their email address
# containing the token necessary for the user to register.
#
# Validates and saves the given user. The invitation will fail if the user is invalid.
#
# @return The invited user or nil if the invitation failed.
def invite_user!(user)
user, token = user_invitation user
if token
OpenProject::Notifications.send(EVENT_NAME, token)
user
end
end
##
# Creates an invited user with the given email address.
# If no first and last is given it will default to 'OpenProject User'
# for the first name and 'To-be' for the last name.
# The default login is the email address.
#
# @return Returns the user and the invitation token required to register.
def user_invitation(user)
User.transaction do
if user.valid?
token = invitation_token user
token.save!
user.invite
user.save!
return [user, token]
end
end
[user, nil]
end
def token_action
'invite'
end
def invitation_token(user)
Token.find_or_initialize_by user: user, action: token_action
end
end

@ -0,0 +1,109 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'uri'
class InvitationsController < ApplicationController
skip_before_filter :check_if_login_required, only: [:claim]
def index
end
def create
email = params.require(:email)
user = User.create mail: email, login: email, firstname: email, lastname: email
token = invite_user user
if user.errors.empty?
first, last = email.split("@")
user.firstname = first
user.lastname = "@#{last}"
user.invite
user.save!
token.save!
puts
puts "CREATED NEW TOKEN: #{token.value}"
puts
redirect_to action: :show, id: user.id
else
flash.now[:error] = user.errors.full_messages.first
render 'index', locals: { email: email }
end
end
def show
user = User.find params.require(:id)
token = Token.find_by action: token_action, user: user
render 'show', locals: { token: token.value, email: user.mail }
end
def claim
token = Token.find_by action: token_action, value: params.require(:id)
if current_user.logged?
flash[:warning] = 'You are already registered, mate.'
redirect_to invitation_path id: token.user_id
elsif token.expired?
flash[:error] = 'The invitation has expired.'
token.destroy
redirect_to signin_path
else
session[:invitation_token] = token.value
flash[:info] = 'Create a new account or register now, pl0x!'
redirect_to signin_path
end
end
module Functions
def token_action
'invitation'
end
def invite_user(user)
token = invitation_token user
token
end
def invitation_token(user)
Token.find_or_initialize_by user: user, action: token_action
end
end
include Functions
end

@ -35,6 +35,8 @@ class MembersController < ApplicationController
before_filter :authorize before_filter :authorize
include Pagination::Controller include Pagination::Controller
include PaginationHelper
paginate_model User paginate_model User
search_for User, :search_in_project search_for User, :search_in_project
search_options_for User, lambda { |_| { project: @project } } search_options_for User, lambda { |_| { project: @project } }
@ -45,6 +47,15 @@ class MembersController < ApplicationController
@@scripts.unshift(script) @@scripts.unshift(script)
end end
def index
@roles = Role.find_all_givable
@members = index_members @project
end
def new
set_roles_and_principles!
end
def create def create
if params[:member] if params[:member]
members = new_members_from_params members = new_members_from_params
@ -52,28 +63,37 @@ class MembersController < ApplicationController
end end
respond_to do |format| respond_to do |format|
if members.present? && members.all?(&:valid?) if members.present? && members.all?(&:valid?)
flash.now.notice = l(:notice_successful_create) flash.notice = members_added_notice members
format.html do redirect_to settings_project_path(@project, tab: 'members') end
format.js do format.html do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project } redirect_to project_members_path(project_id: @project)
render(:update) do |page| end
page.replace_html 'tab-content-members', partial: 'projects/settings/members',
locals: { members: members }
page.insert_html :top, 'tab-content-members', render_flash_messages
page << MembersController.tab_scripts format.js
else
format.html do
if members.present? && params[:member]
@member = members.first
else
flash.error = l(:error_check_user_and_role)
end end
set_roles_and_principles!
render 'new'
end end
else
format.js do format.js do
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project } @pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page| render(:update) do |page|
if params[:member] if params[:member]
page.insert_html :top, 'tab-content-members', partial: 'members/member_errors', locals: { member: members.first } page.replace_html 'new-member-message',
partial: 'members/member_errors',
locals: { member: members.first }
else else
page.insert_html :top, 'tab-content-members', partial: 'members/common_error', locals: { message: l(:error_check_user_and_role) } page.replace_html 'new-member-message',
partial: 'members/common_error',
locals: { message: l(:error_check_user_and_role) }
end end
end end
end end
@ -84,45 +104,31 @@ class MembersController < ApplicationController
def update def update
member = update_member_from_params member = update_member_from_params
if member.save if member.save
flash.now.notice = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
else
# only possible message is about choosing at least one role
flash[:error] = member.errors.full_messages.first
end end
respond_to do |format| redirect_to project_members_path(project_id: @project,
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project, page: params[:page] end page: params[:page],
format.js do per_page: params[:per_page])
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page|
if params[:membership]
@user = member.user
page.replace_html 'tab-content-memberships', partial: 'users/memberships'
else
page.replace_html 'tab-content-members', partial: 'projects/settings/members'
end
page.insert_html :top, 'tab-content-members', render_flash_messages
page << MembersController.tab_scripts
page.visual_effect(:highlight, "member-#{@member.id}") unless Member.find_by(id: @member.id).nil?
end
end
end
end end
def destroy def destroy
if @member.deletable? if @member.deletable?
@member.destroy if @member.disposable?
flash.now.notice = l(:notice_successful_delete) flash.notice = I18n.t(:notice_member_deleted, user: @member.principal.name)
end
respond_to do |format| @member.user.destroy
format.html do redirect_to controller: '/projects', action: 'settings', tab: 'members', id: @project end else
format.js do flash.notice = I18n.t(:notice_member_removed, user: @member.principal.name)
@pagination_url_options = { controller: 'projects', action: 'settings', id: @project }
render(:update) do |page| @member.destroy
page.replace_html 'tab-content-members', partial: 'projects/settings/members'
page.insert_html :top, 'tab-content-members', render_flash_messages
page << MembersController.tab_scripts
end
end end
end end
redirect_to project_members_path(project_id: @project)
end end
def autocomplete_for_member def autocomplete_for_member
@ -140,6 +146,10 @@ class MembersController < ApplicationController
@principals = Principal.possible_members(params[:q], 100) - @project.principals @principals = Principal.possible_members(params[:q], 100) - @project.principals
end end
@email = suggest_invite_via_email? current_user,
params[:q],
(@principals | @project.principals)
respond_to do |format| respond_to do |format|
format.json format.json
format.html do format.html do
@ -158,28 +168,83 @@ class MembersController < ApplicationController
private private
def suggest_invite_via_email?(user, query, principals)
user.admin? && # only admins may add new users via email
query =~ mail_regex &&
principals.none? { |p| p.mail == query } &&
query # finally return email
end
def mail_regex
/\A\S+@\S+\.\S+\z/
end
def index_members(project)
order = User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s).join(', ')
project
.member_principals
.includes(:roles, :principal, :member_roles)
.order(order)
.page(params[:page])
.references(:users)
.per_page(per_page_param)
end
def self.tab_scripts def self.tab_scripts
@@scripts.join('(); ') + '();' @@scripts.join('(); ') + '();'
end end
def set_roles_and_principles!
@roles = Role.find_all_givable
# Check if there is at least one principal that can be added to the project
@principals_available = @project.possible_members('', 1)
end
def new_members_from_params def new_members_from_params
user_ids = possibly_seperated_ids_for_entity(params[:member], :user)
roles = Role.where(id: possibly_seperated_ids_for_entity(params[:member], :role)) roles = Role.where(id: possibly_seperated_ids_for_entity(params[:member], :role))
new_member = lambda { |user_id| if roles.present?
Member.new(permitted_params.member).tap do |member| user_ids = invite_new_users possibly_seperated_ids_for_entity(params[:member], :user)
member.user_id = user_id if user_id members = user_ids.map { |user_id| new_member user_id }
# most likely wrong user input, use a dummy member for error handling
if !members.present? && roles.present?
members << new_member(nil)
end end
}
members
members = user_ids.map { |user_id| else
new_member.call(user_id) # Pick a user that exists but can't be chosen.
} # We only want the missing role error message.
# most likely wrong user input, use a dummy member for error handling dummy = new_member User.anonymous.id
if !members.present? && roles.present?
members << new_member.call(nil) [dummy]
end
end
def new_member(user_id)
Member.new(permitted_params.member).tap do |member|
member.user_id = user_id if user_id
end end
members end
def invite_new_users(user_ids)
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?
# 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) ||
User.find_by_mail(id)
user.id if user
end
else
id
end
end.compact
end end
def each_comma_seperated(array, &block) def each_comma_seperated(array, &block)
@ -195,7 +260,7 @@ class MembersController < ApplicationController
def transform_array_of_comma_seperated_ids(array) def transform_array_of_comma_seperated_ids(array)
return array unless array.present? return array unless array.present?
each_comma_seperated(array) do |elem| each_comma_seperated(array) do |elem|
elem.to_s.split(',').map(&:to_i) elem.to_s.split(',')
end end
end end
@ -221,4 +286,12 @@ class MembersController < ApplicationController
@member.assign_attributes(attrs) @member.assign_attributes(attrs)
@member @member
end end
def members_added_notice(members)
if members.size == 1
l(:notice_member_added, name: members.first.name)
else
l(:notice_members_added, number: members.size)
end
end
end end

@ -120,26 +120,9 @@ class UsersController < ApplicationController
@user = User.new(language: Setting.default_language, mail_notification: Setting.default_notification_option) @user = User.new(language: Setting.default_language, mail_notification: Setting.default_notification_option)
@user.attributes = permitted_params.user_create_as_admin(false, @user.change_password_allowed?) @user.attributes = permitted_params.user_create_as_admin(false, @user.change_password_allowed?)
@user.admin = params[:user][:admin] || false @user.admin = params[:user][:admin] || false
@user.login = params[:user][:login] || @user.mail
if @user.change_password_allowed? if UserInvitation.invite_user! @user
if params[:user][:assign_random_password]
@user.random_password!
else
@user.password = params[:user][:password]
@user.password_confirmation = params[:user][:password_confirmation]
end
end
if @user.save
# TODO: Similar to My#account
@user.pref.attributes = params[:pref] || {}
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
UserMailer.account_information(@user, @user.password).deliver if params[:send_information]
respond_to do |format| respond_to do |format|
format.html do format.html do
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
@ -151,8 +134,6 @@ class UsersController < ApplicationController
end end
else else
@auth_sources = AuthSource.all @auth_sources = AuthSource.all
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format| respond_to do |format|
format.html do render action: 'new' end format.html do render action: 'new' end
@ -187,8 +168,18 @@ class UsersController < ApplicationController
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
if @user.active? && params[:send_information] && !@user.password.blank? && @user.change_password_allowed? if !@user.password.blank? && @user.change_password_allowed?
UserMailer.account_information(@user, @user.password).deliver send_information = params[:send_information]
if @user.invited?
# setting a password for an invited user activates them implicitly
@user.activate!
send_information = true
end
if @user.active? && send_information
UserMailer.account_information(@user, @user.password).deliver
end
end end
respond_to do |format| respond_to do |format|
@ -228,7 +219,12 @@ class UsersController < ApplicationController
# Was the account activated? (do it before User#save clears the change) # Was the account activated? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUSES[:registered], was_activated = (@user.status_change == [User::STATUSES[:registered],
User::STATUSES[:active]]) User::STATUSES[:active]])
if @user.save
if params[:activate] && @user.missing_authentication_method?
flash[:error] = I18n.t(:error_status_change_failed,
errors: I18n.t(:notice_user_missing_authentication_method),
scope: :user)
elsif @user.save
flash[:notice] = I18n.t(:notice_successful_update) flash[:notice] = I18n.t(:notice_successful_update)
if was_activated if was_activated
UserMailer.account_activated(@user).deliver UserMailer.account_activated(@user).deliver

@ -52,12 +52,7 @@ module ProjectsHelper
name: 'modules', name: 'modules',
action: :select_project_modules, action: :select_project_modules,
partial: 'projects/settings/modules', partial: 'projects/settings/modules',
label: :label_module_plural }, label: :label_module_plural
{
name: 'members',
action: :manage_members,
partial: 'projects/settings/members',
label: :label_member_plural
}, },
{ {
name: 'custom_fields', name: 'custom_fields',

@ -0,0 +1,39 @@
class DummyAuthSource < AuthSource
def test_connection
# the dummy connection is always available
end
def authenticate(login, password)
existing_user(login, password) || on_the_fly_user(login)
end
def auth_method_name
'DerpLAP'
end
private
def existing_user(login, password)
registered_login?(login) && password == 'dummy'
end
def on_the_fly_user(login)
return nil unless onthefly_register?
{
firstname: login.capitalize,
lastname: 'Dummy',
mail: 'login@DerpLAP.net',
auth_source_id: id
}
end
def registered_login?(login)
not users.where(login: login).empty? # empty? to use EXISTS query
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
end

@ -46,7 +46,7 @@ class Member < ActiveRecord::Base
after_destroy :unwatch_from_permission_change after_destroy :unwatch_from_permission_change
def name def name
user.name principal.name
end end
def to_s def to_s
@ -130,6 +130,14 @@ class Member < ActiveRecord::Base
@membership @membership
end end
##
# Returns true if this user can be deleted as they have no other memberships
# and haven't been activated yet. Only applies if the member is actually a user
# as opposed to a group.
def disposable?
user && user.invited? && user.memberships.none? { |m| m.project_id != project_id }
end
protected protected
def destroy_if_no_roles_left! def destroy_if_no_roles_left!

@ -36,7 +36,8 @@ class Principal < ActiveRecord::Base
builtin: 0, builtin: 0,
active: 1, active: 1,
registered: 2, registered: 2,
locked: 3 locked: 3,
invited: 4
} }
self.table_name = "#{table_name_prefix}users#{table_name_suffix}" self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
@ -55,7 +56,9 @@ class Principal < ActiveRecord::Base
scope :active, -> { where(status: STATUSES[:active]) } scope :active, -> { where(status: STATUSES[:active]) }
scope :active_or_registered, -> { where(status: [STATUSES[:active], STATUSES[:registered]]) } scope :active_or_registered, -> {
where(status: [STATUSES[:active], STATUSES[:registered], STATUSES[:invited]])
}
scope :active_or_registered_like, ->(query) { active_or_registered.like(query) } scope :active_or_registered_like, ->(query) { active_or_registered.like(query) }

@ -68,7 +68,8 @@ class Project < ActiveRecord::Base
.where("#{Principal.table_name}.type='Group' OR " + .where("#{Principal.table_name}.type='Group' OR " +
"(#{Principal.table_name}.type='User' AND " + "(#{Principal.table_name}.type='User' AND " +
"(#{Principal.table_name}.status=#{Principal::STATUSES[:active]} OR " + "(#{Principal.table_name}.status=#{Principal::STATUSES[:active]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:registered]}))") "#{Principal.table_name}.status=#{Principal::STATUSES[:registered]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:invited]}))")
}, class_name: 'Member' }, class_name: 'Member'
has_many :users, through: :members has_many :users, through: :members
has_many :principals, through: :member_principals, source: :principal has_many :principals, through: :member_principals, source: :principal

@ -215,12 +215,12 @@ class User < Principal
register_allowance_evaluator OpenProject::PrincipalAllowanceEvaluator::Default register_allowance_evaluator OpenProject::PrincipalAllowanceEvaluator::Default
# Returns the user that matches provided login and password, or nil # Returns the user that matches provided login and password, or nil
def self.try_to_login(login, password) def self.try_to_login(login, password, session = nil)
# Make sure no one can sign in with an empty password # Make sure no one can sign in with an empty password
return nil if password.to_s.empty? return nil if password.to_s.empty?
user = find_by_login(login) user = find_by_login(login)
user = if user user = if user
try_authentication_for_existing_user(user, password) try_authentication_for_existing_user(user, password, session)
else else
try_authentication_and_create_user(login, password) try_authentication_and_create_user(login, password)
end end
@ -233,8 +233,11 @@ class User < Principal
# Tries to authenticate a user in the database via external auth source # Tries to authenticate a user in the database via external auth source
# or password stored in the database # or password stored in the database
def self.try_authentication_for_existing_user(user, password) def self.try_authentication_for_existing_user(user, password, session = nil)
activate_user! user, session if session
return nil if !user.active? || OpenProject::Configuration.disable_password_login? return nil if !user.active? || OpenProject::Configuration.disable_password_login?
if user.auth_source if user.auth_source
# user has an external authentication method # user has an external authentication method
return nil unless user.auth_source.authenticate(user.login, password) return nil unless user.auth_source.authenticate(user.login, password)
@ -247,6 +250,19 @@ class User < Principal
user user
end end
def self.activate_user!(user, session)
if session[:invitation_token]
token = Token.find_by_value session[:invitation_token]
invited_id = token && token.user.id
if user.id == invited_id
user.activate!
token.destroy!
session.delete :invitation_token
end
end
end
# Tries to authenticate with available sources and creates user on success # Tries to authenticate with available sources and creates user on success
def self.try_authentication_and_create_user(login, password) def self.try_authentication_and_create_user(login, password)
return nil if OpenProject::Configuration.disable_password_login? return nil if OpenProject::Configuration.disable_password_login?
@ -328,6 +344,10 @@ class User < Principal
self.status = STATUSES[:registered] self.status = STATUSES[:registered]
end end
def invite
self.status = STATUSES[:invited]
end
def lock def lock
self.status = STATUSES[:locked] self.status = STATUSES[:locked]
end end
@ -340,6 +360,14 @@ class User < Principal
update_attribute(:status, STATUSES[:registered]) update_attribute(:status, STATUSES[:registered])
end end
def invite!
update_attribute(:status, STATUSES[:invited])
end
def invited?
status == STATUSES[:invited]
end
def lock! def lock!
update_attribute(:status, STATUSES[:locked]) update_attribute(:status, STATUSES[:locked])
end end
@ -707,6 +735,17 @@ class User < Principal
User.current.admin? ? Role.all : User.current.roles_for_project(project) User.current.admin? ? Role.all : User.current.roles_for_project(project)
end end
##
# Returns true if no authentication method has been chosen for this user yet.
# There are three possible methods currently:
#
# - username & password
# - OmniAuth
# - LDAP
def missing_authentication_method?
identity_url.nil? && passwords.empty? && auth_source.nil?
end
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
# one anonymous user per database. # one anonymous user per database.
def self.anonymous def self.anonymous

@ -39,12 +39,13 @@ See doc/COPYRIGHT.rdoc for more details.
auth_provider_html = call_hook :view_account_login_auth_provider auth_provider_html = call_hook :view_account_login_auth_provider
no_pwd = OpenProject::Configuration.disable_password_login? no_pwd = OpenProject::Configuration.disable_password_login?
pclass = no_pwd ? 'no-pwd' : '' pclass = no_pwd ? 'no-pwd' : ''
wclass = local_assigns[:wide] ? 'wide' : ''
%> %>
<% if auth_provider_html.strip != '' %> <% if auth_provider_html.strip != '' %>
<div class="login-auth-providers <%= pclass %>"> <div class="login-auth-providers <%= pclass %> <%= wclass %>">
<% unless no_pwd %> <% unless no_pwd %>
<h3 class="login-auth-providers-title"><span> <h3 class="login-auth-providers-title"><span>
<%= I18n.t('account.login_with_auth_provider')%> <%= local_assigns[:omniauth_title] || I18n.t('account.login_with_auth_provider')%>
</span></h3> </span></h3>
<% end %> <% end %>
<div class="login-auth-provider-list"> <div class="login-auth-provider-list">

@ -33,14 +33,14 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="form--field"> <div class="form--field">
<%= styled_label_tag 'username', User.human_attribute_name(:login) %> <%= styled_label_tag 'username', User.human_attribute_name(:login) %>
<div class="form--field-container"> <div class="form--field-container">
<%= styled_text_field_tag 'username', nil, tabindex: 1 %> <%= styled_text_field_tag 'username', params[:username], tabindex: 1 %>
</div> </div>
</div> </div>
<div class="form--field"> <div class="form--field">
<%= styled_label_tag 'password', User.human_attribute_name(:password) %> <%= styled_label_tag 'password', User.human_attribute_name(:password) %>
<div class="form--field-container"> <div class="form--field-container">
<%= styled_password_field_tag 'password', nil, tabindex: 2 %> <%= styled_password_field_tag 'password', nil, tabindex: 2, autofocus: params.include?(:username) %>
</div> </div>
</div> </div>

@ -83,4 +83,6 @@ See doc/COPYRIGHT.rdoc for more details.
</section> </section>
<%= styled_button_tag l(:button_submit), class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_submit), class: '-highlight -with-icon icon-yes' %>
<%= render partial: 'auth_providers', locals: { omniauth_title: I18n.t('account.signup_with_auth_provider'), wide: true } %>
<% end %> <% end %>

@ -27,19 +27,28 @@ See doc/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<% html_title t(:label_member_new) %>
<%= toolbar title: t(:label_member_new) %>
<%= labelled_tabular_form_for(:member, <%= labelled_tabular_form_for(:member,
:url => {:controller => '/members', :action => 'create', :project_id => project}, :url => {:controller => '/members', :action => 'create', :project_id => project},
:method => :post, :method => :post,
:remote => true,
:loading => '$(\'member-add-submit\').disable();', :loading => '$(\'member-add-submit\').disable();',
:complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable(); activateFlashError();', :complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable(); activateFlashError();',
:html => {:id => "members_add_form"}) do |f| %> :html => {:id => "members_add_form"}) do |f| %>
<div class="form--section"> <div class="form--section">
<h3 class="form--section-title"><%= l(:label_member_new) %></h3> <div id="new-member-message"></div>
<div class="grid-block medium-up-2"> <div class="grid-block medium-up-2">
<div class="form--column"> <div class="form--column">
<div class="form--field"> <div class="form--field">
<%= styled_label_tag :principal_search, l(:label_principal_search) %> <%
user_id_title = I18n.t(:label_principal_search)
if current_user.admin?
user_id_title += I18n.t(:label_principal_invite_via_email)
end
%>
<%= styled_label_tag :principal_search, user_id_title %>
<%= styled_text_field_tag :principal_search, nil %> <%= styled_text_field_tag :principal_search, nil %>
<%= observe_field(:principal_search, <%= observe_field(:principal_search,
:frequency => 0.5, :frequency => 0.5,
@ -63,6 +72,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= f.button l(:button_add), :id => 'member-add-submit', <%= f.button l(:button_add), :id => 'member-add-submit',
class: 'button -highlight -with-icon icon-yes' %> class: 'button -highlight -with-icon icon-yes' %>
<% end %> <% end %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div> </div>
<% end %> <% end %>

@ -29,19 +29,29 @@ See doc/COPYRIGHT.rdoc for more details.
<%= javascript_include_tag "members_select_boxes.js" %> <%= javascript_include_tag "members_select_boxes.js" %>
<% html_title I18n.t(:label_member_new) %>
<%= toolbar title: I18n.t(:label_member_new) %>
<%= labelled_tabular_form_for(:member, :url => {:controller => 'members', :action => 'create', :project_id => project}, <%= labelled_tabular_form_for(:member, :url => {:controller => 'members', :action => 'create', :project_id => project},
:remote => true,
:method => :post, :method => :post,
:html => {:id => "members_add_form"}) do |f| %> :html => {:id => "members_add_form"}) do |f| %>
<div class="form--section"> <div class="form--section">
<h3 class="form--section-title"><%= l(:label_member_new) %></h3> <div id="new-member-message"></div>
<div class="grid-block medium-up-2"> <div class="grid-block medium-up-2">
<div class="form--column"> <div class="form--column">
<div class="form--field -vertical"> <div class="form--field -vertical">
<%= styled_label_tag :member_user_ids, l(:label_principal_search) %> <%
user_id_title = I18n.t(:label_principal_search)
if current_user.admin?
user_id_title += I18n.t(:label_principal_invite_via_email)
end
%>
<%= styled_label_tag :member_user_ids, user_id_title %>
<%= select_tag "member[user_ids]", options_for_select([]), <%= select_tag "member[user_ids]", options_for_select([]),
:title => l(:label_principal_search), :title => user_id_title,
:multiple => true, :multiple => true,
:autofocus => true,
:'data-ajaxURL' => url_for(:controller => "/members", :action => "autocomplete_for_member"), :'data-ajaxURL' => url_for(:controller => "/members", :action => "autocomplete_for_member"),
:'data-projectId' => project.id, :'data-projectId' => project.id,
no_label: true, no_label: true,
@ -61,7 +71,8 @@ See doc/COPYRIGHT.rdoc for more details.
</div> </div>
</div> </div>
</div> </div>
<%= f.button l(:button_add), :id => 'member-add-submit', <%= f.button l(:button_add), id: 'member-add-submit',
class: 'button -highlight -with-icon icon-yes' %> class: 'button -highlight -with-icon icon-yes' %>
<%= link_to I18n.t('button_cancel'), :back, class: 'button' %>
</div> </div>
<% end %> <% end %>

@ -27,16 +27,29 @@ See doc/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<%#
Lists found users (principals) to be used for the select box.
If the user enters an email address, a 'result' in the form of
'Invite: <email>' will be returned.
#%>
{ {
"results": "results":
{ {
"items":[ "items":[
<% @principals.each_with_index do |principal, ix| %> <% @principals.each_with_index do |principal, ix| %>
{ {
"id": <%= principal.id.to_json.html_safe %>, "id": <%= principal.id.to_json.html_safe %>,
"name": <%= principal.name.to_json.html_safe %> "name": <%= principal.name.to_json.html_safe %>
} <%= "," unless ix == @principals.length - 1 %> } <%= "," unless !@email && ix == @principals.length - 1 %>
<% end %> ], <% end %>
<% if @email %>
{
"id": "<%= @email %>",
"name": "Invite <%= @email %>"
}
<% end %>
],
"total": <%= @total ? @total : @principals.size %>, "total": <%= @total ? @total : @principals.size %>,
"more": <%= @more ? @more : 0 %> "more": <%= @more ? @more : 0 %>
} }

@ -0,0 +1,4 @@
// reload iframes parent (project members view)
// to show updated list of members and a flash message
window.parent.document.location.reload();

@ -0,0 +1,155 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
++#%>
<% html_title 'Members' %>
<%= toolbar title: 'Members' do %>
<% if authorize_for(:members, :new) %>
<a href="<%= new_project_member_path %>" id="add-member-button" title="Add Member" class="button -alt-highlight">
<i class="button--icon icon-add"></i>
<span class="button--text"><%= I18n.t(:button_add_member) %></span>
</a>
<% end %>
<% end %>
<div>
<% if @members.any? %>
<% authorized = authorize_for('members', 'update') %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table interactive-table role="grid" class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<%= call_hook(:view_projects_settings_members_table_colgroup, :project => @project) %>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= User.model_name.human %> / <%= Group.model_name.human %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= l(:label_role_plural) %>
</span>
</div>
</div>
</th>
<%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
<th></th>
</tr>
</thead>
<tbody>
<% @members.each do |member| %>
<% next if member.new_record? %>
<tr id="member-<%= member.id %>" class=" member">
<%
member_type = member.principal.class.name.downcase
classes = [
member_type,
('icon icon-group' if member_type == 'group'),
user_status_class(member.principal)
].compact.join(' ')
%>
<td class="<%= classes %>" title="<%= user_status_i18n member.principal %>">
<%= link_to_user member.principal %>
<% if member.user && member.user.invited? %>
<i title="<%= t('text_user_invited') %>" class="icon icon-mail"></i>
<% end %>
</td>
<td class="roles">
<span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
<% if authorized %>
<%= form_for(member, :url => {:controller => '/members',
:action => 'update',
:id => member,
:page => params[:page],
:per_page => params[:per_page] },
:method => :put,
:html => { :id => "member-#{member.id}-roles-form",
:class => 'hol',
:style => 'display:none' }) do |f| %>
<p><% @roles.each do |role| %>
<label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
:disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label>
<% end %></p>
<%= hidden_field_tag 'member[role_ids][]', '' %>
<p><%= submit_tag l(:button_change), :class => 'button -highlight -small' %>
<%= link_to_function l(:button_cancel),
"$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;",
class: 'button -small' %></p>
<% end %>
<%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
<td class="buttons">
<%
delete_class, delete_title = if member.disposable?
['icon icon-delete', I18n.t(:title_remove_and_delete_user)]
else
['icon icon-close', I18n.t(:button_remove)]
end
%>
<%= link_to_function '', "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit', :title => l(:button_edit) %>
<%= link_to('', {:controller => '/members', :action => 'destroy', :id => member, :page => params[:page]},
:method => :delete,
data: { confirm: ((!User.current.admin? && member.include?(User.current)) ? l(:text_own_membership_delete_confirmation) : nil) },
:title => delete_title, :class => delete_class) if member.deletable? %>
</td>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="generic-table--header-background"></div>
</div>
</div>
<%= pagination_links_full @members, @pagination_url_options || {} %>
<% else %>
<div class="generic-table--container">
<div class="generic-table--no-results-container">
<h2 class="generic-table--no-results-title">
<i class="icon-info"></i>
<%= l(:label_nothing_display) %>
</h2>
<div class="generic-table--no-results-description">
<p class="nodata"><%= l(:label_no_data) %></p>
</div>
</div>
</div>
<% end %>
</div>

@ -0,0 +1,43 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
++#%>
<%= javascript_include_tag "members_form.js" %>
<%= error_messages_for 'member' %>
<div>
<% if @principals_available.any? %>
<%= render :partial => "members/member_form",
:locals => { :project => @project, :roles => @roles } %>
<% else %>
<p>
<%= I18n.t('text_no_roles_defined') %>
</p>
<%= link_to I18n.t('button_back'), :back, class: 'button' %>
<% end %>
</div>

@ -1,147 +0,0 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
++#%>
<%= javascript_include_tag "members_form.js" %>
<%= error_messages_for 'member' %>
<% roles = Role.find_all_givable
# Check if there is at least one principal that can be added to the project
principals_available = @project.possible_members("", 1)
member_per_page = 20
@members = @project.member_principals.includes(:roles, :principal, :member_roles)
.order(User::USER_FORMATS_STRUCTURE[Setting.user_format].map{|attr| attr.to_s}.join(", "))
.page(params[:page])
.references(:users)
.per_page(per_page_param)
%>
<div>
<% if roles.any? && principals_available.any? %>
<%= render :partial => "members/member_form",
:locals => { :project => @project,
:members => @members,
:roles => roles } %>
<% end %>
</div>
<div>
<% if @members.any? %>
<% authorized = authorize_for('members', 'update') %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table interactive-table role="grid" class="generic-table">
<colgroup>
<col highlight-col>
<col highlight-col>
<%= call_hook(:view_projects_settings_members_table_colgroup, :project => @project) %>
<col>
</colgroup>
<thead>
<tr>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= User.model_name.human %> / <%= Group.model_name.human %>
</span>
</div>
</div>
</th>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= l(:label_role_plural) %>
</span>
</div>
</div>
</th>
<%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
<th></th>
</tr>
</thead>
<tbody>
<% @members.each do |member| %>
<% next if member.new_record? %>
<tr id="member-<%= member.id %>" class=" member">
<td class="<%= member.principal.class.name.downcase %> <%= 'icon icon-group' if member.principal.class.name.downcase == 'group' %> <%= user_status_class member.principal%>" title="<%= user_status_i18n member.principal%>"><%= link_to_user member.principal %></td>
<td class="roles">
<span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
<% if authorized %>
<%= form_for(member, :url => {:controller => '/members',
:action => 'update',
:id => member,
:page => params[:page]},
:method => :put,
:remote => true,
:html => { :id => "member-#{member.id}-roles-form",
:class => 'hol',
:style => 'display:none' }) do |f| %>
<p><% roles.each do |role| %>
<label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
:disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label>
<% end %></p>
<%= hidden_field_tag 'member[role_ids][]', '' %>
<p><%= submit_tag l(:button_change), :class => 'button -highlight -small' %>
<%= link_to_function l(:button_cancel),
"$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;",
class: 'button -small' %></p>
<% end %>
<%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
<td class="buttons">
<%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
<%= link_to(l(:button_delete), {:controller => '/members', :action => 'destroy', :id => member, :page => params[:page]},
:method => :delete,
:remote => true,
data: { confirm: ((!User.current.admin? && member.include?(User.current)) ? l(:text_own_membership_delete_confirmation) : nil) },
:title => l(:button_delete), :class => 'icon icon-delete') if member.deletable? %>
</td>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="generic-table--header-background"></div>
</div>
</div>
<%= pagination_links_full @members, params: { tab: 'members' }.merge(@pagination_url_options || {}) %>
<% else %>
<div class="generic-table--container">
<div class="generic-table--no-results-container">
<h2 class="generic-table--no-results-title">
<i class="icon-info"></i>
<%= l(:label_nothing_display) %>
</h2>
<div class="generic-table--no-results-description">
<p class="nodata"><%= l(:label_no_data) %></p>
</div>
</div>
</div>
<% end %>
</div>

@ -0,0 +1,60 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
++#%>
<%= error_messages_for 'user' %>
<%= javascript_include_tag 'admin_users' %>
<!--[form:user]-->
<section class="form--section">
<div class="form--field"><%= f.text_field :mail, required: true, autofocus: true %></div>
<div class="form--field"><%= f.text_field :firstname, required: true %></div>
<div class="form--field"><%= f.text_field :lastname, required: true %></div>
<%= render partial: 'customizable/field',
collection: @user.custom_field_values,
as: :value,
locals: { form: f } %>
<% unless @auth_sources.empty? || OpenProject::Configuration.disable_password_login? %>
<div class="form--field">
<% sources = ([[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] }) %>
<%= f.select :auth_source_id, sources %>
</div>
<div class="form--field" id="new_user_login" style="display: none;">
<%= f.text_field :login, :required => true, :size => 25, :disabled => true %>
</div>
<% end %>
<div class="form--field"><%= f.check_box :admin, :disabled => (@user == User.current) %></div>
<%= call_hook(:view_users_form, :user => @user, :form => f) %>
</section>
<!--[eoform:user]-->

@ -33,11 +33,8 @@ See doc/COPYRIGHT.rdoc for more details.
:url => { :action => "create" }, :url => { :action => "create" },
:html => { :class => nil, :autocomplete => 'off' }, :html => { :class => nil, :autocomplete => 'off' },
:as => :user do |f| %> :as => :user do |f| %>
<%= render :partial => 'form', :locals => { :f => f } %> <%= render :partial => 'simple_form', :locals => { f: f, auth_sources: @auth_sources, user: @user } %>
<div class="form--field">
<label><%= styled_check_box_tag 'send_information', 1, true %>
<%= l(:label_send_information) %></label>
</div>
<p> <p>
<%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_create), class: '-highlight -with-icon icon-yes' %>
<%= styled_button_tag l(:button_create_and_continue), :name => 'continue', class: '-highlight -with-icon icon-yes' %> <%= styled_button_tag l(:button_create_and_continue), :name => 'continue', class: '-highlight -with-icon icon-yes' %>

@ -267,6 +267,12 @@ Redmine::MenuManager.map :project_menu do |menu|
if: Proc.new { |p| p.project_type.try :allows_association }, if: Proc.new { |p| p.project_type.try :allows_association },
html: { class: 'icon2 icon-dependency' } html: { class: 'icon2 icon-dependency' }
menu.push :members,
{ controller: :members, action: :index },
param: :project_id,
caption: :label_member_plural,
html: { class: 'icon2 icon-group' }
menu.push :settings, menu.push :settings,
{ controller: '/projects', action: 'settings' }, { controller: '/projects', action: 'settings' },
caption: :label_project_settings, caption: :label_project_settings,

@ -54,8 +54,11 @@ Redmine::AccessControl.map do |map|
require: :member require: :member
map.permission :manage_members, map.permission :manage_members,
{ projects: :settings, { members: [:index, :new, :create, :update, :destroy, :autocomplete_for_member] },
members: [:create, :update, :destroy, :autocomplete_for_member] }, require: :member
map.permission :view_members,
{ members: [:index] },
require: :member require: :member
map.permission :manage_versions, map.permission :manage_versions,

@ -0,0 +1,6 @@
##
# The default behaviour is to send the user a sign-up mail
# when they were invited.
OpenProject::Notifications.subscribe UserInvitation::EVENT_NAME do |token|
UserMailer.user_signed_up(token).deliver
end

@ -27,14 +27,15 @@
#++ #++
en: en:
label_invitation: Invitation
account: account:
delete: "Delete account" delete: "Delete account"
delete_confirmation: "Are you sure you want to delete the account?" delete_confirmation: "Are you sure you want to delete the account?"
deleted: "Account successfully deleted" deleted: "Account successfully deleted"
deletion_info: deletion_info:
data_consequences: data_consequences:
other: "Of the data the user created (e.g. e-mail, preferences, work packages, wiki entries) as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called \"Deleted user\". As the data of every deleted account is reassigned to this account it will not be possible to distinguish the data the user created from the data of another deleted account." other: "Of the data the user created (e.g. email, preferences, work packages, wiki entries) as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called \"Deleted user\". As the data of every deleted account is reassigned to this account it will not be possible to distinguish the data the user created from the data of another deleted account."
self: "Of the data you created (e.g. e-mail, preferences, work packages, wiki entries) as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called \"Deleted user\". As the data of every deleted account is reassigned to this account it will not be possible to distinguish the data you created from the data of another deleted account." self: "Of the data you created (e.g. email, preferences, work packages, wiki entries) as much as possible will be deleted. Note however, that data like work packages and wiki entries can not be deleted without impeding the work of the other users. Such data is hence reassigned to an account called \"Deleted user\". As the data of every deleted account is reassigned to this account it will not be possible to distinguish the data you created from the data of another deleted account."
heading: "Delete account %{name}" heading: "Delete account %{name}"
info: info:
other: "Deleting the user account is an irreversible action." other: "Deleting the user account is an irreversible action."
@ -52,6 +53,10 @@ en:
User registration is disabled on this system. Please ask an administrator to create an User registration is disabled on this system. Please ask an administrator to create an
account for you. account for you.
login_with_auth_provider: "or sign in with your existing account" login_with_auth_provider: "or sign in with your existing account"
signup_with_auth_provider: "or sign up using"
signup_with_password: "using user name and password."
auth_source_login: Please login as <em>%{login}</em> to activate your account.
omniauth_login: Please login to activate your account.
actionview_instancetag_blank_option: "Please select" actionview_instancetag_blank_option: "Please select"
@ -356,6 +361,7 @@ en:
work_package: "Work package" work_package: "Work package"
button_add: "Add" button_add: "Add"
button_add_member: Add member
button_add_watcher: "Add watcher" button_add_watcher: "Add watcher"
button_annotate: "Annotate" button_annotate: "Annotate"
button_apply: "Apply" button_apply: "Apply"
@ -387,6 +393,7 @@ en:
button_move: "Move" button_move: "Move"
button_move_and_follow: "Move and follow" button_move_and_follow: "Move and follow"
button_quote: "Quote" button_quote: "Quote"
button_remove: Remove
button_remove_widget: "Remove widget" button_remove_widget: "Remove widget"
button_rename: "Rename" button_rename: "Rename"
button_reply: "Reply" button_reply: "Reply"
@ -937,7 +944,8 @@ en:
label_preview: "Preview" label_preview: "Preview"
label_previous: "Previous" label_previous: "Previous"
label_previous_week: "Previous week" label_previous_week: "Previous week"
label_principal_search: "Search for users or groups" label_principal_invite_via_email: " or invite new users via email"
label_principal_search: "Add existing users or groups"
label_professional_support: "Professional support" label_professional_support: "Professional support"
label_profile: "Profile" label_profile: "Profile"
label_project_all: "All Projects" label_project_all: "All Projects"
@ -993,7 +1001,7 @@ en:
label_role_and_permissions: "Roles and permissions" label_role_and_permissions: "Roles and permissions"
label_role_new: "New role" label_role_new: "New role"
label_role_plural: "Roles" label_role_plural: "Roles"
label_role_search: "Search for roles" label_role_search: "Assign roles to new members"
label_scm: "SCM" label_scm: "SCM"
label_search: "Search" label_search: "Search"
label_search_titles_only: "Search titles only" label_search_titles_only: "Search titles only"
@ -1173,6 +1181,8 @@ en:
noscript_learn_more: "Learn more" noscript_learn_more: "Learn more"
notice_account_activated: "Your account has been activated. You can now log in." notice_account_activated: "Your account has been activated. You can now log in."
notice_account_already_activated: The account has already been activated.
notice_account_invalid_token: Invalid activation token
notice_account_invalid_credentials: "Invalid user or password" notice_account_invalid_credentials: "Invalid user or password"
notice_account_invalid_credentials_or_blocked: "Invalid user or password or the account is blocked due to multiple failed login attempts. If so, it will be unblocked automatically in a short time." notice_account_invalid_credentials_or_blocked: "Invalid user or password or the account is blocked due to multiple failed login attempts. If so, it will be unblocked automatically in a short time."
notice_account_lost_email_sent: "An email with instructions to choose a new password has been sent to you." notice_account_lost_email_sent: "An email with instructions to choose a new password has been sent to you."
@ -1186,6 +1196,7 @@ en:
notice_account_updated: "Account was successfully updated." notice_account_updated: "Account was successfully updated."
notice_account_wrong_password: "Wrong password" notice_account_wrong_password: "Wrong password"
notice_account_registered_and_logged_in: "Welcome, your account has been activated. You are logged in now." notice_account_registered_and_logged_in: "Welcome, your account has been activated. You are logged in now."
notice_activation_failed: The account could not be activated.
notice_api_access_key_reseted: "Your API access key was reset." notice_api_access_key_reseted: "Your API access key was reset."
notice_can_t_change_password: "This account uses an external authentication source. Impossible to change the password." notice_can_t_change_password: "This account uses an external authentication source. Impossible to change the password."
notice_email_error: "An error occurred while sending mail (%{value})" notice_email_error: "An error occurred while sending mail (%{value})"
@ -1200,6 +1211,10 @@ en:
notice_locking_conflict: "Information has been updated by at least one other user in the meantime." notice_locking_conflict: "Information has been updated by at least one other user in the meantime."
notice_locking_conflict_additional_information: "The update(s) came from %{users}." notice_locking_conflict_additional_information: "The update(s) came from %{users}."
notice_locking_conflict_reload_page: "Please reload the page, review the changes and reapply your updates." notice_locking_conflict_reload_page: "Please reload the page, review the changes and reapply your updates."
notice_member_added: Added %{name} to the project.
notice_members_added: Added %{number} users to the the project.
notice_member_removed: "Removed %{user} from project."
notice_member_deleted: "%{user} has been removed from the project and deleted."
notice_no_principals_found: "No results found." notice_no_principals_found: "No results found."
notice_bad_request: "Bad Request." notice_bad_request: "Bad Request."
notice_not_authorized: "You are not authorized to access this page." notice_not_authorized: "You are not authorized to access this page."
@ -1212,8 +1227,10 @@ en:
notice_to_many_principals_to_display: "There are too many results.\nNarrow down the search by typing in the name of the new member (or group)." notice_to_many_principals_to_display: "There are too many results.\nNarrow down the search by typing in the name of the new member (or group)."
notice_unable_delete_time_entry: "Unable to delete time log entry." notice_unable_delete_time_entry: "Unable to delete time log entry."
notice_unable_delete_version: "Unable to delete version." notice_unable_delete_version: "Unable to delete version."
notice_user_missing_authentication_method: User has yet to chose a password or another way the sign in.
notice_automatic_set_of_standard_type: "Set standard type automatically." notice_automatic_set_of_standard_type: "Set standard type automatically."
notice_logged_out: "You have been logged out." notice_logged_out: "You have been logged out."
notice_wont_delete_auth_source: The authentication mode cannot be deleted as long as there are still users using it.
error_types_in_use_by_work_packages: "The following types are still referenced by work packages: %{types}" error_types_in_use_by_work_packages: "The following types are still referenced by work packages: %{types}"
notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields." notice_project_cannot_update_custom_fields: "You cannot update the project's available custom fields."
@ -1514,6 +1531,8 @@ en:
show_hide_project_menu: "Show/Hide project menu" show_hide_project_menu: "Show/Hide project menu"
status_active: "active" status_active: "active"
status_invited: invited
status_registered: registered
# Used in array.to_sentence. # Used in array.to_sentence.
support: support:
@ -1563,6 +1582,7 @@ en:
text_line_separated: "Multiple values allowed (one line for each value)." text_line_separated: "Multiple values allowed (one line for each value)."
text_load_default_configuration: "Load the default configuration" text_load_default_configuration: "Load the default configuration"
text_min_max_length_info: "0 means no restriction" text_min_max_length_info: "0 means no restriction"
text_no_roles_defined: There are no roles defined.
text_no_configuration_data: "Roles, types, work package statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." text_no_configuration_data: "Roles, types, work package statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
text_no_notes: "There are no comments available for this work package." text_no_notes: "There are no comments available for this work package."
text_notice_too_many_values_are_inperformant: "Note: Displaying more than 100 items per page can increase the page load time." text_notice_too_many_values_are_inperformant: "Note: Displaying more than 100 items per page can increase the page load time."
@ -1585,6 +1605,7 @@ en:
text_tip_work_package_end_day: "work package ending this day" text_tip_work_package_end_day: "work package ending this day"
text_type_no_workflow: "No workflow defined for this type" text_type_no_workflow: "No workflow defined for this type"
text_unallowed_characters: "Unallowed characters" text_unallowed_characters: "Unallowed characters"
text_user_invited: The user has been invited and is pending registration.
text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. work packages you're the author or assignee)." text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. work packages you're the author or assignee)."
text_user_wrote: "%{value} wrote:" text_user_wrote: "%{value} wrote:"
text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page." text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
@ -1796,6 +1817,8 @@ en:
quarters: "Quarters" quarters: "Quarters"
years: "Years" years: "Years"
title_remove_and_delete_user: Remove the invited user from the project and delete them.
queries: queries:
apply_filter: Apply preconfigured filter apply_filter: Apply preconfigured filter
@ -1812,7 +1835,7 @@ en:
authentication_settings_disabled_due_to_external_authentication: > authentication_settings_disabled_due_to_external_authentication: >
This user authenticates via an external authentication provider, so there is no password This user authenticates via an external authentication provider, so there is no password
in OpenProject to be changed. in OpenProject to be changed.
assign_random_password: "Assign random password (sent to user via e-mail)" assign_random_password: "Assign random password (sent to user via email)"
blocked: "locked temporarily" blocked: "locked temporarily"
blocked_num_failed_logins: blocked_num_failed_logins:
one: "locked temporarily (one failed login attempt)" one: "locked temporarily (one failed login attempt)"
@ -1822,6 +1845,7 @@ en:
lock: "Lock permanently" lock: "Lock permanently"
locked: "locked permanently" locked: "locked permanently"
registered: "registered" registered: "registered"
invited: invited
reset_failed_logins: "Reset failed logins" reset_failed_logins: "Reset failed logins"
status_user_and_brute_force: "%{user} and %{brute_force}" status_user_and_brute_force: "%{user} and %{brute_force}"
unlock: "Unlock" unlock: "Unlock"
@ -1829,6 +1853,7 @@ en:
no_login: "This user authenticates through login by password. Since it is disabled, they cannot log in." no_login: "This user authenticates through login by password. Since it is disabled, they cannot log in."
password_change_unsupported: Change of password is not supported. password_change_unsupported: Change of password is not supported.
authorization_rejected: "You are not allowed to sign in." authorization_rejected: "You are not allowed to sign in."
invite: Invite user via email
version_status_closed: "closed" version_status_closed: "closed"
version_status_locked: "locked" version_status_locked: "locked"

@ -45,7 +45,7 @@ OpenProject::Application.routes.draw do
get '/account/force_password_change', action: 'force_password_change' get '/account/force_password_change', action: 'force_password_change'
post '/account/change_password', action: 'change_password' post '/account/change_password', action: 'change_password'
match '/account/lost_password', action: 'lost_password', via: [:get, :post] match '/account/lost_password', action: 'lost_password', via: [:get, :post]
match '/account/register', action: 'register', via: [:get, :post] match '/account/register', action: 'register', via: [:get, :post, :patch]
# omniauth routes # omniauth routes
match '/auth/:provider/callback', action: 'omniauth_login', match '/auth/:provider/callback', action: 'omniauth_login',
@ -329,7 +329,7 @@ OpenProject::Application.routes.draw do
resources :categories, except: [:index, :show], shallow: true resources :categories, except: [:index, :show], shallow: true
resources :members, only: [:create, :update, :destroy], shallow: true do resources :members, only: [:index, :new, :create, :update, :destroy], shallow: true do
match :autocomplete_for_member, on: :collection, via: [:get, :post] match :autocomplete_for_member, on: :collection, via: [:get, :post]
end end
@ -578,6 +578,11 @@ OpenProject::Application.routes.draw do
resources :reported_project_statuses, controller: 'reported_project_statuses' resources :reported_project_statuses, controller: 'reported_project_statuses'
resources :invitations, controller: 'invitations', only: [:index, :show, :create]
scope controller: 'invitations' do
get 'claim/:id', action: 'claim'
end
# This route should probably be removed, but it's used at least by one cuke and we don't # This route should probably be removed, but it's used at least by one cuke and we don't
# want to break it. # want to break it.
# This route intentionally occurs after the admin/roles/new route, so that one takes # This route intentionally occurs after the admin/roles/new route, so that one takes

@ -84,7 +84,8 @@ else
:browse_repository, :browse_repository,
:view_changesets, :view_changesets,
:commit_access, :commit_access,
:view_commit_author_statistics] :view_commit_author_statistics,
:view_members]
end.save! end.save!
Role.new.tap do |reader| Role.new.tap do |reader|

@ -1,186 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
Feature: Group Memberships
Background:
Given I am already admin
Given there is a role "Manager"
And there is a role "Developer"
And there is 1 project with the following:
| Name | Project1 |
| Identifier | project1 |
And there is 1 User with:
| Login | peter |
| Firstname | Peter |
| Lastname | Pan |
And there is 1 User with:
| Login | hannibal |
| Firstname | Hannibal |
| Lastname | Smith |
And there is a group named "A-Team" with the following members:
| peter |
| hannibal |
@javascript
Scenario: Adding a group to a project on the project's page adds the group members as well
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I add the principal "A-Team" as a member with the roles:
| Manager |
Then I should be on the "members" tab of the settings page of the project called "Project1"
And I should see "A-Team" within ".generic-table"
And I should see "Hannibal Smith" within ".generic-table"
And I should see "Peter Pan" within ".generic-table"
@javascript
Scenario: Group-based memberships and individual memberships are handled separately
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I add the principal "Hannibal Smith" as a member with the roles:
| Manager |
And I wait for the AJAX requests to finish
Then I should see "Successful creation." within ".flash.notice"
And I add the principal "A-Team" as a member with the roles:
| Developer |
And I wait for the AJAX requests to finish
Then I should see "Successful creation." within ".flash.notice"
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should see "Hannibal Smith" within ".generic-table"
And I should not see "A-Team" within ".generic-table"
And I should not see "Peter Pan" within ".generic-table"
@javascript
Scenario: Removing a group from a project on the project's page removes all group members as well
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I add the principal "A-Team" as a member with the roles:
| Manager |
Then I should be on the "members" tab of the settings page of the project called "Project1"
And I wait for the AJAX requests to finish
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should see "Nothing to display"
And I should not see "A-Team" within ".generic-table--container"
And I should not see "Hannibal Smith" within ".generic-table--container"
And I should not see "Peter Pan" within ".generic-table--container"
@javascript
Scenario: Adding a user to a group adds the user to projects as well
When I go to the admin page of the group called "A-Team"
And I click on "tab-users"
And I delete "hannibal" from the group
And I wait for the AJAX requests to finish
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add" within "#tab-content-memberships"
And I wait for the AJAX requests to finish
And I click on "tab-users"
And I check "Hannibal Smith"
And I press "Add" within "#tab-content-users"
And I wait for the AJAX requests to finish
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
Then I should see "A-Team" within ".generic-table"
And I should see "Peter Pan" within ".generic-table"
And I should see "Hannibal Smith" within ".generic-table"
@javascript
Scenario: Removing a user from a group removes the user from projects as well
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add" within "#tab-content-memberships"
And I wait for the AJAX requests to finish
When I click on "tab-users"
And I delete "hannibal" from the group
And I wait for the AJAX requests to finish
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
Then I should see "A-Team" within ".generic-table"
And I should not see "Hannibal Smith" within ".generic-table"
And I should see "Peter Pan" within ".generic-table"
@javascript
Scenario: Adding a group to project on the group's page adds the group members as well
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add" within "#tab-content-memberships"
And I wait for the AJAX requests to finish
Then the project member "A-Team" should have the role "Manager"
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
Then I should see "A-Team" within ".generic-table"
And I should see "Hannibal Smith" within ".generic-table"
And I should see "Peter Pan" within ".generic-table"
@javascript
Scenario: Adding/Removing a group to/from a project displays success message
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add" within "#tab-content-memberships"
And I wait for the AJAX requests to finish
Then I should see "Successful update." within ".notice"
And I should see "Project1"
When I follow "Delete" within "table.generic-table.memberships"
Then I should see "Successful deletion." within ".notice"
And I should see "No data to display"

@ -1,99 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
Feature: Group memberships
Background:
Given there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And there is 1 user with the following:
| login | bob |
| firstname | Bob |
| Lastname | Bobbit |
And there is 1 user with the following:
| login | alice |
| firstname | Alice |
| lastname | Wonderland |
And there is 1 group with the following:
| name | group1 |
And there is a role "alpha"
And there is a role "beta"
And the role "alpha" may have the following rights:
| manage_members |
And the user "bob" is a "alpha" in the project "project1"
@javascript
Scenario: Adding a group with members to a project
Given the group "group1" has the following members:
| alice |
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I add the principal "group1" as a member with the roles:
| beta |
Then I should see the principal "group1" as a member with the roles:
| beta |
And I should see the principal "alice" as a member with the roles:
| beta |
Scenario: Adding members to a group after the group has been added to the project adds the users to the project
Given the group "group1" is a "beta" in the project "project1"
And I am already admin
When I go to the edit page of the group called "group1"
And I follow "Users" within ".tabs"
And I add the user "alice" to the group
And I go to the members tab of the settings page of the project "project1"
Then I should see the principal "group1" as a member with the roles:
| beta |
And I should see the principal "alice" as a member with the roles:
| beta |
@javascript
Scenario: Removing a group from a project removes its members (users) as well if they have no roles of their own
Given the group "group1" has the following members:
| alice |
And the group "group1" is a "beta" in the project "project1"
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I follow the delete link of the project member "group1"
Then I should not see the principal "group1" as a member
And I should not see the principal "alice" as a member
@javascript
Scenario: Removing a group from a project leaves a member if he has other roles besides those inherited from the group
Given the group "group1" has the following members:
| alice |
And the user "alice" is a "alpha" in the project "project1"
And the group "group1" is a "beta" in the project "project1"
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I follow the delete link of the project member "group1"
Then I should not see the principal "group1" as a member
And I should see the principal "alice" as a member with the roles:
| alpha |

@ -1,114 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
Feature: Error Messages
Background:
Given I am already admin
Given there is a role "Manager"
And there is 1 project with the following:
| Name | Project1 |
| Identifier | project1 |
And there is 1 User with:
| Login | peter |
| Firstname | Peter |
| Lastname | Pan |
@javascript
Scenario: Adding a Principal, non impaired
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I select the principal "Peter Pan"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should see 1 error message
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should not see 2 error messages
@javascript
Scenario: Adding a Role, non impaired
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I select the role "Manager"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should see 1 error message
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should not see 2 error messages
@javascript
Scenario: Adding a Principal, impaired
When I am impaired
And I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I select the principal "Peter Pan"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should see 1 error message
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should not see 2 error messages
@javascript
Scenario: Adding a Role, impaired
When I am impaired
And I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I select the role "Manager"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should see 1 error message
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should not see 2 error messages
@javascript
Scenario: Removing old error or success messages when adding members
Given there is 1 User with:
| Login | tinkerbell |
| Firstname | Tinker |
| Lastname | Bell |
When I go to the settings page of the project called "Project1"
And I click on "tab-members"
And I select the principal "Peter Pan"
And I select the role "Manager"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then there should be a flash notice message
And there should not be any error message
Then I select the principal "Tinker Bell"
And I click on "Add" within "#tab-content-members"
And I wait for AJAX
Then I should see 1 error message
And there should not be a flash notice message

@ -1,154 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
Feature: Membership
Background:
Given I am already admin
Given there is a role "Manager"
And there is a role "Developer"
And there is 1 project with the following:
| Identifier | project1 |
And there is 1 User with:
| Login | peter |
| Firstname | Peter |
| Lastname | Pan |
And there is 1 User with:
| Login | hannibal |
| Firstname | Hannibal |
| Lastname | Smith |
And there is 1 User with:
| Login | crash |
| Firstname | <script>alert('h4x');</script> |
| Lastname | <script>alert('h4x');</script> |
And there is a group named "A-Team" with the following members:
| peter |
| hannibal |
@javascript
Scenario: Adding and Removing a Group as Member, non impaired
When I go to the members tab of the settings page of the project "project1"
And I add the principal "A-Team" as "Manager"
Then I should be on the members tab of the settings page of the project "project1"
And I should see "Successful creation." within ".flash.notice"
And I should see "A-Team" within ".generic-table"
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should see "Nothing to display"
@javascript
Scenario: Adding and removing a User as Member, non impaired
When I go to the members tab of the settings page of the project "project1"
And I add the principal "Hannibal Smith" as "Manager"
Then I should see "Successful creation." within ".flash.notice"
And I should see "Hannibal Smith" within ".generic-table"
When I delete the "Hannibal Smith" membership
And I wait for the AJAX requests to finish
Then I should see "Nothing to display"
@javascript
Scenario: Entering a Username as Member in firstname, lastname order, non impaired
When I go to the members tab of the settings page of the project "project1"
And I enter the principal name "Hannibal S"
Then I should see "Hannibal Smith"
@javascript
Scenario: Entering a Username as Member in lastname, firstname order, non impaired
When I go to the members tab of the settings page of the project "project1"
And I enter the principal name "Smith, H"
Then I should see "Hannibal Smith"
@javascript
Scenario: Escaping should work properly when entering a name
When I go to the members tab of the settings page of the project "project1"
And I enter the principal name "script"
Then I should not see an alert dialog
And I should see "<script>alert('h4x');</script>"
@javascript
Scenario: Escaping should work properly when selecting a user
When I go to the members tab of the settings page of the project "project1"
When I select the principal "script"
Then I should not see an alert dialog
And I should see "<script>alert('h4x');</script>"
@javascript
Scenario: Adding and Removing a Group as Member, impaired
When I am impaired
And I go to the members tab of the settings page of the project "project1"
And I add the principal "A-Team" as "Manager"
And I go to the members tab of the settings page of the project "project1"
Then I should not see "A-Team" within "#principal_results"
And I should see "A-Team" within ".generic-table"
@javascript
Scenario: User should not appear in members form if he/she is already a member of the project, impaired
When I am impaired
And I go to the members tab of the settings page of the project "project1"
And I add the principal "A-Team" as "Manager"
Then I should be on the members tab of the settings page of the project "project1"
And I should see "Successful creation." within ".flash.notice"
And I should see "A-Team" within ".generic-table"
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should see "Nothing to display"
@javascript
Scenario: Entering a Username as Member in firstname, lastname order, impaired
When I am impaired
And I go to the members tab of the settings page of the project "project1"
And I enter the principal name "Hannibal S"
Then I should see "Hannibal Smith"
@javascript
Scenario: Entering a Username as Member in lastname, firstname order, impaired
When I am impaired
And I go to the members tab of the settings page of the project "project1"
And I enter the principal name "Smith, H"
Then I should see "Hannibal Smith"
@javascript
Scenario: Adding and removing a User as Member, impaired
When I am impaired
And I go to the members tab of the settings page of the project "project1"
And I add the principal "Hannibal Smith" as "Manager"
Then I should see "Successful creation." within ".flash.notice"
And I should see "Hannibal Smith" within ".generic-table"
When I delete the "Hannibal Smith" membership
And I wait for the AJAX requests to finish
Then I should see "Nothing to display"

@ -1,79 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
Feature: Membership
Background:
Given I am already admin
Given there is a role "Manager"
And there is a role "Developer"
And there is 1 project with the following:
| Identifier | project1 |
And there is 1 User with:
| Login | peter |
| Firstname | Peter |
| Lastname | Pan |
And there is 1 user with the following:
| login | bob |
| firstname | Bob |
| Lastname | Bobbit |
And there is 1 user with the following:
| login | alice |
| firstname | Alice |
| Lastname | Alison |
And the user "bob" is a "Manager" in the project "project1"
And the user "alice" is a "Developer" in the project "project1"
@javascript
Scenario: Paginating after adding a member
Given we paginate after 2 items
When I go to the members tab of the settings page of the project "project1"
And I add the principal "peter" as "Manager"
When I follow "2" within ".pagination"
Then I should see "Peter Pan"
@javascript
Scenario: Paginating after removing a member
Given we paginate after 1 items
And the user "peter" is a "Manager" in the project "project1"
When I go to the members tab of the settings page of the project "project1"
And I delete the "Alice Alison" membership
Then I should see "Bob Bobbit"
When I follow "2" within ".pagination"
Then I should see "Peter Pan"
@javascript
Scenario: Paginating after updating a member
Given we paginate after 1 items
When I go to the members tab of the settings page of the project "project1"
And I click on "Edit"
And I check "Manager"
And I click "Change"
And I follow "2" within ".pagination"
Then I should see "Bob Bobbit"

@ -31,57 +31,8 @@ Feature: Project Settings
Given there is 1 project with the following: Given there is 1 project with the following:
| name | project1 | | name | project1 |
| identifier | project1 | | identifier | project1 |
And there is 1 user with the following:
| login | bob |
| firstname | Bob |
| Lastname | Bobbit |
And there is 1 user with the following:
| login | alice |
| firstname | Alice |
| Lastname | Alison |
And there is a role "alpha"
And there is a role "beta"
And the user "bob" is a "alpha" in the project "project1"
And the user "alice" is a "beta" in the project "project1"
Given I am already admin Given I am already admin
@javascript
Scenario: Adding a Role to a Member
When I go to the members tab of the settings page of the project "project1"
When I click on "Edit" within "#member-1"
And I check "beta" within "#member-1-roles-form"
And I click "Change" within "#member-1-roles-form"
Then I should see "alpha" within "#member-1-roles"
And I should see "beta" within "#member-1-roles"
@javascript
Scenario: Removing one Role from while adding another Role to a Member
When I go to the members tab of the settings page of the project "project1"
When I click on "Edit" within "#member-1"
And I uncheck "alpha" within "#member-1-roles-form"
And I check "beta" within "#member-1-roles-form"
And I click "Change" within "#member-1-roles-form"
Then I should see "beta" within "#member-1-roles"
And I should not see "alpha" within "#member-1-roles"
@javascript
Scenario: Removing the last Role of a Member
When I go to the members tab of the settings page of the project "project1"
When I click on "Edit" within "#member-1"
And I uncheck "alpha" within "#member-1-roles-form"
And I click "Change" within "#member-1-roles-form"
Then there should be an error message
And I should see "Bob Bobbit" within ".generic-table"
And I should see "alpha" within ".generic-table"
Scenario: Changing members per page keeps us on the members tab
When I go to the settings page of the project "project1"
And I follow "Members" within ".tabs"
And I follow "20" within ".pagination--options" within "#tab-content-members"
Then I should be on the members tab of the settings page of the project "project1"
Scenario: Adding a Work Package custom field to the project Scenario: Adding a Work Package custom field to the project
When the following issue custom fields are defined: When the following issue custom fields are defined:
| name | type | is_for_all | | name | type | is_for_all |

@ -31,15 +31,12 @@ Feature: Adding a user
Scenario: as an admin a user can be created Scenario: as an admin a user can be created
Given I am already admin Given I am already admin
When I go to the new user page When I go to the new user page
And I fill in "psmith" for "user_login"
And I fill in "Paul" for "user_firstname" And I fill in "Paul" for "user_firstname"
And I fill in "Smith" for "user_lastname" And I fill in "Smith" for "user_lastname"
And I fill in "psmith@somenet.foo" for "user_mail" And I fill in "psmith@somenet.foo" for "user_mail"
And I fill in "psmithPSMITH09" for "user_password"
And I fill in "psmithPSMITH09" for "user_password_confirmation"
And I submit the form by the "Create" button And I submit the form by the "Create" button
Then I should see "Successful creation" Then I should see "Successful creation"
And I should be on the edit page of the user "psmith" And I should be on the edit page of the user "psmith@somenet.foo"
When I logout When I logout
And I login as "psmith" with password "psmithPSMITH09" And I login as "psmith@somenet.foo" with password "psmithPSMITH09"
Then I should be logged in as "psmith" Then I should see "Your account has not yet been activated."

@ -40,18 +40,6 @@ Feature: User Status
Then there should be a flash error message Then there should be a flash error message
And there should be a "New password" field And there should be a "New password" field
@javascript
Scenario: New user can be assigned a random password
When I create a new user
And I check the assign random password to user field
And I save the new user
Then an e-mail should be sent containing "Password"
When I try to log in with user "newbobby"
Then I should not see "Bob Bobbit"
When I try to log in with user "newbobby" and the password sent via email
Then there should be a flash error message
And there should be a "New password" field
@javascript @javascript
Scenario: Password fields are disabled and cleared when random password assignment is activated Scenario: Password fields are disabled and cleared when random password assignment is activated
When I edit the user "bobby" When I edit the user "bobby"

@ -31,13 +31,29 @@ module OpenProject
# Subscribe to a specific event with name # Subscribe to a specific event with name
# Contrary to ActiveSupport::Notifications, we don't support regexps here, but only # Contrary to ActiveSupport::Notifications, we don't support regexps here, but only
# single events specified as string. # single events specified as string.
def self.subscribe(name, &block) #
# @param name [String] The name of the event to subscribe to.
# @param clear_subscriptions [Boolean] Clears all previous subscriptions to this
# event if true. Use with care!
# @return nil
# @raises ArgumentError if no block is given.
def self.subscribe(name, clear_subscriptions: false, &block)
# if no block is given, raise an error # if no block is given, raise an error
raise ArgumentError, 'please provide a block as a callback' unless block_given? raise ArgumentError, 'please provide a block as a callback' unless block_given?
ActiveSupport::Notifications.subscribe(name.to_s) do |_name, _start, _finish, _id, payload| if clear_subscriptions
subscriptions[name].each do |sub|
ActiveSupport::Notifications.unsubscribe sub
end
end
sub = ActiveSupport::Notifications.subscribe(name.to_s) do |_, _, _, _, payload|
block.call(payload) block.call(payload)
end end
subs = clear_subscriptions ? [] : Array(subscriptions[name])
subscriptions[name] = subs + [sub]
# Don't return a subscription object as it's an implementation detail. # Don't return a subscription object as it's an implementation detail.
nil nil
end end
@ -49,5 +65,11 @@ module OpenProject
def self.send(name, payload) def self.send(name, payload)
ActiveSupport::Notifications.instrument(name, payload) ActiveSupport::Notifications.instrument(name, payload)
end end
def self.subscriptions
@subscriptions ||= {}
end
private_class_method :subscriptions
end end
end end

@ -33,7 +33,7 @@ module OpenProject
def link_to_user(user, options = {}) def link_to_user(user, options = {})
if user.is_a?(User) if user.is_a?(User)
name = user.name(options.delete(:format)) name = user.name(options.delete(:format))
if user.active? || user.registered? if user.active? || user.registered? || user.invited?
link_to(name, user, options) link_to(name, user, options)
else else
name name

@ -0,0 +1,60 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
desc 'Creates a dummy LDAP auth source for logging in any user using the password "dummy".'
namespace :ldap do
task create_dummy: :environment do
source_name = 'DerpLAP'
otf_reg = ARGV.include?('onthefly_register')
source = DummyAuthSource.create name: source_name, onthefly_register: otf_reg
puts
if source.valid?
puts "Created dummy auth source called \"#{source_name}\""
puts 'On-the-fly registration support: ' + otf_reg.to_s
unless otf_reg
puts "use `rake ldap:create_dummy[onthefly_register]` to enable on-the-fly registration"
end
else
puts "Dummy auth source already exists. It's called \"#{source_name}\"."
end
puts
puts 'Note: Dummy auth sources cannot be edited, so clicking on them'
puts " in the 'LDAP Authentication' view will result in an error. Bummer!"
end
task delete_dummies: :environment do
DummyAuthSource.destroy_all
puts
puts 'Deleted all dummy auth sources. Users who used it are out of luck! :o'
end
end

@ -31,7 +31,7 @@ require 'spec_helper'
describe MembersController, type: :controller do describe MembersController, type: :controller do
let(:admin) { FactoryGirl.create(:admin) } let(:admin) { FactoryGirl.create(:admin) }
let(:user) { FactoryGirl.create(:user) } let(:user) { FactoryGirl.create(:user) }
let(:project) { FactoryGirl.create(:project) } let(:project) { FactoryGirl.create(:project, identifier: 'pet_project') }
let(:role) { FactoryGirl.create(:role) } let(:role) { FactoryGirl.create(:role) }
let(:member) { let(:member) {
FactoryGirl.create(:member, project: project, FactoryGirl.create(:member, project: project,
@ -136,36 +136,30 @@ describe MembersController, type: :controller do
let(:user2) { FactoryGirl.create(:user) } let(:user2) { FactoryGirl.create(:user) }
let(:user3) { FactoryGirl.create(:user) } let(:user3) { FactoryGirl.create(:user) }
let(:user4) { FactoryGirl.create(:user) } let(:user4) { FactoryGirl.create(:user) }
let(:valid_params) {
{ format: 'js',
project_id: project.id,
member: { role_ids: [role.id],
user_ids: [user2.id, user3.id, user4.id] } }
}
let(:invalid_params) {
{ format: 'js',
project_id: project.id,
member: { role_ids: [],
user_ids: [user2.id, user3.id, user4.id] } }
}
context 'post :create' do context 'post :create' do
context 'single member' do context 'single member' do
let(:action) { post :create, project_id: project.id, member: { role_ids: [role.id], user_id: user2.id } } let(:action) do
post :create, project_id: project.id, member: { role_ids: [role.id], user_id: user2.id }
end
it 'should add a member' do it 'should add a member' do
expect { action }.to change { Member.count }.by(1) expect { action }.to change { Member.count }.by(1)
expect(response).to redirect_to(settings_project_path(project) + '/members') expect(response).to redirect_to '/projects/pet_project/members'
expect(user2).to be_member_of(project) expect(user2).to be_member_of(project)
end end
end end
context 'multiple members' do context 'multiple members' do
let(:action) { post :create, project_id: project.id, member: { role_ids: [role.id], user_ids: [user2.id, user3.id, user4.id] } } let(:action) do
post :create,
project_id: project.id,
member: { role_ids: [role.id], user_ids: [user2.id, user3.id, user4.id] }
end
it 'should add all members' do it 'should add all members' do
expect { action }.to change { Member.count }.by(3) expect { action }.to change { Member.count }.by(3)
expect(response).to redirect_to(settings_project_path(project) + '/members') expect(response).to redirect_to '/projects/pet_project/members'
expect(user2).to be_member_of(project) expect(user2).to be_member_of(project)
expect(user3).to be_member_of(project) expect(user3).to be_member_of(project)
expect(user4).to be_member_of(project) expect(user4).to be_member_of(project)
@ -173,36 +167,23 @@ describe MembersController, type: :controller do
end end
end end
context 'post :create in JS format' do context 'with a failed save' do
context 'with successful saves' do let(:invalid_params) {
before do { project_id: project.id,
post :create, valid_params member: { role_ids: [],
end user_ids: [user2.id, user3.id, user4.id] } }
}
it 'should add members' do
expect(user2).to be_member_of(project)
expect(user3).to be_member_of(project)
expect(user4).to be_member_of(project)
end
it 'should replace the tab with RJS' do before do
assert_select_rjs :replace_html, 'tab-content-members' post :create, invalid_params
end
end end
end
context 'with a failed save' do it 'should not redirect to the members index' do
it 'should not replace the tab with RJS' do expect(response).not_to redirect_to '/projects/pet_project/members'
post :create, invalid_params
assert_select '#tab-content-members', 0
end end
it 'should show an error message' do it 'should show an error message' do
post :create, invalid_params expect(response.body).to include 'choose at least one role'
assert_select_rjs :insert_html, :top do
assert_select '#errorExplanation'
end
end end
end end
end end
@ -215,7 +196,7 @@ describe MembersController, type: :controller do
it 'should destroy a member' do it 'should destroy a member' do
expect { action }.to change { Member.count }.by(-1) expect { action }.to change { Member.count }.by(-1)
expect(response).to redirect_to(settings_project_path(project) + '/members') expect(response).to redirect_to '/projects/pet_project/members'
expect(user).not_to be_member_of(project) expect(user).not_to be_member_of(project)
end end
end end
@ -230,7 +211,7 @@ describe MembersController, type: :controller do
it 'should update the member' do it 'should update the member' do
expect { action }.not_to change { Member.count } expect { action }.not_to change { Member.count }
expect(response).to redirect_to(settings_project_path(project) + '/members') expect(response).to redirect_to '/projects/pet_project/members'
end end
end end
end end

@ -36,4 +36,8 @@ FactoryGirl.define do
port 225 # a reserved port, should not be in use port 225 # a reserved port, should not be in use
attr_login 'uid' attr_login 'uid'
end end
factory :dummy_auth_source, class: DummyAuthSource do
name 'DerpLAP'
end
end end

@ -0,0 +1,96 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'group memberships through groups page', type: :feature do
let!(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let!(:peter) { FactoryGirl.create :user, firstname: 'Peter', lastname: 'Pan' }
let!(:hannibal) { FactoryGirl.create :user, firstname: 'Hannibal', lastname: 'Smith' }
let(:group) { FactoryGirl.create :group, lastname: 'A-Team' }
let!(:manager) { FactoryGirl.create :role, name: 'Manager' }
let!(:developer) { FactoryGirl.create :role, name: 'Developer' }
let(:members_page) { Pages::Members.new project.identifier }
let(:group_page) { Pages::Groups.new.group(group.id) }
before do
allow(User).to receive(:current).and_return admin
group.add_member! peter
end
scenario 'adding a user to a group adds the user to the project as well', js: true do
members_page.visit!
expect(members_page).not_to have_user 'Hannibal Smith'
group_page.visit!
group_page.add_to_project! 'Project 1', as: 'Manager'
expect(page).to have_text 'Successful update'
group_page.add_user! 'Hannibal'
members_page.visit!
expect(members_page).to have_user 'A-Team', roles: ['Manager']
expect(members_page).to have_user 'Peter Pan', roles: ['Manager']
expect(members_page).to have_user 'Hannibal Smith', roles: ['Manager']
end
context 'given a group with members in a project' do
before do
group.add_member! hannibal
project.add_member! group, [manager]
end
scenario 'removing a user from the group removes them from the project too' do
members_page.visit!
expect(members_page).to have_user 'Hannibal Smith'
group_page.visit!
group_page.remove_user! 'Hannibal Smith'
members_page.visit!
expect(members_page).to have_user 'A-Team'
expect(members_page).to have_user 'Peter Pan'
expect(members_page).not_to have_user 'Hannibal Smith'
end
scenario 'removing the group from a project', js: true do
group_page.visit!
group_page.open_projects_tab!
expect(group_page).to have_project 'Project 1'
group_page.remove_from_project! 'Project 1'
expect(page).to have_text 'Successful deletion'
expect(page).to have_text 'No data to display'
end
end
end

@ -26,15 +26,25 @@
# See doc/COPYRIGHT.rdoc for more details. # See doc/COPYRIGHT.rdoc for more details.
#++ #++
Feature: Simple deletion of a group require 'spec_helper'
Background: feature 'group memberships through groups page', type: :feature do
Given We have the group "Bob's Team" let(:admin) { FactoryGirl.create :admin }
let!(:group) { FactoryGirl.create :group, lastname: "Bob's Team" }
@javascript let(:groups_page) { Pages::Groups.new }
Scenario: An admin can delete an existing group
Given I am admin context 'as an admin' do
Given I am on the groups administration page before do
And I click on "Delete" allow(User).to receive(:current).and_return admin
And I accept the alert dialog end
Then I should not see "Bob's Team" within "#content"
scenario 'I can delete a group' do
groups_page.visit!
expect(groups_page).to have_group "Bob's Team"
groups_page.delete_group! "Bob's Team"
expect(groups_page).not_to have_group "Bob's Team"
end
end
end

@ -0,0 +1,126 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'group memberships through project members page', type: :feature do
let(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let(:alice) { FactoryGirl.create :user, firstname: 'Alice', lastname: 'Wonderland' }
let(:bob) { FactoryGirl.create :user, firstname: 'Bob', lastname: 'Bobbit' }
let(:group) { FactoryGirl.create :group, lastname: 'group1' }
let!(:alpha) { FactoryGirl.create :role, name: 'alpha', permissions: [:manage_members] }
let!(:beta) { FactoryGirl.create :role, name: 'beta' }
let(:members_page) { Pages::Members.new project.identifier }
let(:groups_page) { Pages::Groups.new }
before do
FactoryGirl.create :member, user: bob, project: project, roles: [alpha]
end
context 'given a group with members' do
before do
allow(User).to receive(:current).and_return bob
group.add_member! alice
end
scenario 'adding group1 as a member with the beta role', js: true do
members_page.visit!
members_page.add_user! 'group1', as: 'beta'
expect(members_page).to have_added_user 'group1'
expect(members_page).to have_user('Alice Wonderland', group_membership: true)
end
context 'which has has been added to a project' do
before do
project.add_member! group, [beta]
end
context 'with the members having no roles of their own' do
scenario 'removing the group removes its members too' do
members_page.visit!
expect(members_page).to have_user('Alice Wonderland')
members_page.remove_user! 'group1'
expect(page).to have_text('Removed group1 from project')
expect(members_page).not_to have_user('group1')
expect(members_page).not_to have_user('Alice Wonderland')
end
end
context 'with the members having roles of their own' do
before do
project.members
.select { |m| m.user_id == alice.id }
.each { |m| m.add_and_save_role alpha }
end
scenario 'removing the group leaves the user without their group roles' do
members_page.visit!
expect(members_page).to have_user('Alice Wonderland', roles: ['alpha', 'beta'])
members_page.remove_user! 'group1'
expect(page).to have_text('Removed group1 from project')
expect(members_page).not_to have_user('group1')
expect(members_page).to have_user('Alice Wonderland', roles: ['alpha'])
expect(members_page).not_to have_roles('Alice Wonderland', ['beta'])
end
end
end
end
context 'given an empty group in a project' do
before do
alice # create alice
project.add_member! group, [beta]
allow(User).to receive(:current).and_return admin
end
scenario 'adding members to that group adds them to the project too' do
members_page.visit!
expect(members_page).not_to have_user('Alice Wonderland') # Alice not in the project yet
expect(members_page).to have_user('group1') # the group is already there though
groups_page.visit!
groups_page.add_user_to_group! 'Alice Wonderland', 'group1'
members_page.visit!
expect(members_page).to have_user('Alice Wonderland', roles: ['beta'])
end
end
end

@ -0,0 +1,73 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'group memberships through groups page', type: :feature do
let!(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let!(:peter) { FactoryGirl.create :user, firstname: 'Peter', lastname: 'Pan' }
let!(:manager) { FactoryGirl.create :role, name: 'Manager' }
let(:members_page) { Pages::Members.new project.identifier }
before do
allow(User).to receive(:current).and_return admin
end
shared_examples 'errors when adding members' do
scenario 'adding a principal without a role, non impaired', js: true do
members_page.visit!
members_page.add_user! 'Peter Pan', as: nil
expect(page).to have_text 'choose at least one role'
end
scenario 'adding a role without a principal, non impaired', js: true do
members_page.visit!
members_page.add_user! nil, as: 'Manager'
expect(page).to have_text 'choose at least one user or group'
end
end
context 'with an impaired user' do
before do
admin.impaired = true
admin.save!
end
it_behaves_like 'errors when adding members'
end
context 'with an un-impaired user' do
it_behaves_like 'errors when adding members'
end
end

@ -0,0 +1,127 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'group memberships through groups page', type: :feature, js: true do
let!(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let!(:peter) { FactoryGirl.create :user, firstname: 'Peter', lastname: 'Pan' }
let!(:hannibal) { FactoryGirl.create :user, firstname: 'Hannibal', lastname: 'Smith' }
let!(:crash) { FactoryGirl.create :user, firstname: "<script>alert('h4x');</script>",
lastname: "<script>alert('h4x');</script>" }
let(:group) { FactoryGirl.create :group, lastname: 'A-Team' }
let!(:manager) { FactoryGirl.create :role, name: 'Manager' }
let!(:developer) { FactoryGirl.create :role, name: 'Developer' }
let(:members_page) { Pages::Members.new project.identifier }
before do
allow(User).to receive(:current).and_return admin
group.add_member! peter
group.add_member! hannibal
end
shared_examples 'adding and removing principals' do
scenario 'Adding and Removing a Group as Member' do
members_page.visit!
members_page.add_user! 'A-Team', as: 'Manager'
expect(members_page).to have_added_user 'A-Team'
members_page.remove_user! 'A-Team'
expect(page).to have_text 'Removed A-Team from project'
expect(page).to have_text 'Nothing to display'
end
scenario 'Adding and removing a User as Member' do
members_page.visit!
members_page.add_user! 'Hannibal Smith', as: 'Manager'
expect(members_page).to have_added_user 'Hannibal Smith'
members_page.remove_user! 'Hannibal Smith'
expect(page).to have_text 'Removed Hannibal Smith from project'
expect(page).to have_text 'Nothing to display'
end
scenario 'Entering a Username as Member in firstname, lastname order' do
members_page.visit!
members_page.open_new_member!
members_page.enter_principal_search! 'Hannibal S'
expect(page).to have_text 'Hannibal Smith'
end
scenario 'Entering a Username as Member in lastname, firstname order' do
members_page.visit!
members_page.open_new_member!
members_page.enter_principal_search! 'Smith, H'
expect(page).to have_text 'Hannibal Smith'
end
scenario 'Escaping should work properly when entering a name' do
members_page.visit!
members_page.open_new_member!
members_page.enter_principal_search! 'script'
expect(members_page).not_to have_alert_dialog
expect(page).to have_text "<script>alert('h4x');</script>"
end
end
context 'with an impaired user' do
before do
admin.impaired = true
admin.save!
end
it_behaves_like 'adding and removing principals'
end
context 'with an un-impaired user' do
it_behaves_like 'adding and removing principals'
# The following scenario is only tested with an unimpaired user
# as it does not make a difference whether or not the user is impaired.
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).not_to have_alert_dialog
expect(page).to have_text "<script>alert('h4x');</script>"
end
end
end

@ -0,0 +1,84 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'members pagination', type: :feature, js: true do
let!(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let!(:peter) { FactoryGirl.create :user, firstname: 'Peter', lastname: 'Pan' }
let!(:bob) { FactoryGirl.create :user, firstname: 'Bob', lastname: 'Bobbit' }
let!(:alice) { FactoryGirl.create :user, firstname: 'Alice', lastname: 'Alison' }
let!(:manager) { FactoryGirl.create :role, name: 'Manager' }
let!(:developer) { FactoryGirl.create :role, name: 'Developer' }
let(:members_page) { Pages::Members.new project.identifier }
before do
allow(User).to receive(:current).and_return admin
project.add_member! bob, [manager]
project.add_member! alice, [developer]
end
scenario 'paginating after adding a member' do
members_page.set_items_per_page! 2
members_page.visit!
members_page.add_user! 'Peter Pan', as: 'Manager'
members_page.go_to_page! 2
expect(members_page).to have_user 'Peter Pan'
end
scenario 'Paginating after removing a member' do
project.add_member! peter, [manager]
members_page.set_items_per_page! 1
members_page.visit!
members_page.remove_user! 'Alice Alison'
expect(members_page).to have_user 'Bob Bobbit'
members_page.go_to_page! 2
expect(members_page).to have_user 'Peter Pan'
end
scenario 'Paginating after updating a member' do
members_page.set_items_per_page! 1
members_page.visit!
members_page.edit_user! 'Alice Alison', add_roles: ['Manager']
expect(page).to have_text 'Successful update'
expect(members_page).to have_user 'Alice Alison', roles: ['Developer', 'Manager']
members_page.go_to_page! 2
expect(members_page).to have_user 'Bob Bobbit'
end
end

@ -0,0 +1,71 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
feature 'members pagination', type: :feature, js: true do
let!(:project) { FactoryGirl.create :project, name: 'Project 1', identifier: 'project1' }
let(:admin) { FactoryGirl.create :admin }
let!(:bob) { FactoryGirl.create :user, firstname: 'Bob', lastname: 'Bobbit' }
let!(:alice) { FactoryGirl.create :user, firstname: 'Alice', lastname: 'Alison' }
let!(:alpha) { FactoryGirl.create :role, name: 'alpha' }
let!(:beta) { FactoryGirl.create :role, name: 'beta' }
let(:members_page) { Pages::Members.new project.identifier }
before do
allow(User).to receive(:current).and_return admin
project.add_member! alice, [beta]
project.add_member! bob, [alpha]
members_page.visit!
end
scenario 'Adding a Role to Alice' do
members_page.edit_user! 'Alice Alison', add_roles: ['alpha']
expect(members_page).to have_user('Alice Alison', roles: ['alpha', 'beta'])
end
scenario 'Adding a role while taking another role away from Alice' do
members_page.edit_user! 'Alice Alison', add_roles: ['alpha'], remove_roles: ['beta']
expect(members_page).to have_user('Alice Alison', roles: 'alpha')
expect(members_page).not_to have_roles('Alice Alison', ['beta'])
end
scenario "Removing Bob's last role results in an error" do
members_page.edit_user! 'Bob Bobbit', remove_roles: ['alpha']
expect(page).to have_text 'choose at least one role'
expect(members_page).to have_user('Bob Bobbit', roles: ['alpha'])
end
end

@ -31,13 +31,17 @@ require 'spec_helper'
describe 'create users', type: :feature do describe 'create users', type: :feature do
let(:current_user) { FactoryGirl.create :admin } let(:current_user) { FactoryGirl.create :admin }
let(:auth_source) { FactoryGirl.build :auth_source } let(:auth_source) { FactoryGirl.build :dummy_auth_source }
before do before do
allow(User).to receive(:current).and_return current_user allow(User).to receive(:current).and_return current_user
end end
shared_examples_for 'successful user creation' do shared_examples_for 'successful user creation' do
let(:mail) { ActionMailer::Base.deliveries.last }
let(:mail_body) { mail.body.parts.first.to_s }
let(:token) { mail_body.scan(/token=(.*)$/).first.first }
it 'creates the user' do it 'creates the user' do
expect(page).to have_selector('.flash', 'Successfully created.') expect(page).to have_selector('.flash', 'Successfully created.')
@ -45,23 +49,48 @@ describe 'create users', type: :feature do
expect(current_path).to eql(edit_user_path(new_user.id)) expect(current_path).to eql(edit_user_path(new_user.id))
end end
it 'sends out an activation email' do
expect(mail_body).to include 'activate your account'
expect(token).not_to be_nil
end
end end
context 'with internal authentication' do context 'with internal authentication' do
before do before do
visit new_user_path visit new_user_path
fill_in 'Login', with: 'bob'
fill_in 'First name', with: 'bobfirst' fill_in 'First name', with: 'bobfirst'
fill_in 'Last name', with: 'boblast' fill_in 'Last name', with: 'boblast'
fill_in 'Email', with: 'bob@mail.com' fill_in 'Email', with: 'bob@mail.com'
fill_in 'Password', with: 'BobBobBob123'
fill_in 'Confirmation', with: 'BobBobBob123'
click_button 'Create' click_button 'Create'
end end
it_behaves_like 'successful user creation' it_behaves_like 'successful user creation' do
describe 'activation' do
before do
allow(User).to receive(:current).and_call_original
visit "/account/activate?token=#{token}"
end
it 'shows the registration form' do
expect(page).to have_text 'Create a new account'
end
it 'registers the user upon submission' do
fill_in 'user_password', with: 'foobarbaz1'
fill_in 'user_password_confirmation', with: 'foobarbaz1'
click_button 'Submit'
# landed on the 'my page'
expect(page).to have_text 'your account has been activated'
expect(page).to have_text 'Login: bob@mail.com'
end
end
end
end end
context 'with external authentication', js: true do context 'with external authentication', js: true do
@ -70,15 +99,39 @@ describe 'create users', type: :feature do
visit new_user_path visit new_user_path
fill_in 'Login', with: 'bob'
fill_in 'First name', with: 'bobfirst' fill_in 'First name', with: 'bobfirst'
fill_in 'Last name', with: 'boblast' fill_in 'Last name', with: 'boblast'
fill_in 'Email', with: 'bob@mail.com' fill_in 'Email', with: 'bob@mail.com'
select auth_source.name, from: 'Authentication mode' select auth_source.name, from: 'Authentication mode'
fill_in 'Login', with: 'bob'
click_button 'Create' click_button 'Create'
end end
it_behaves_like 'successful user creation' it_behaves_like 'successful user creation' do
describe 'activation' do
before do
allow(User).to receive(:current).and_call_original
visit "/account/activate?token=#{token}"
end
it 'shows the login form prompting the user to login' do
expect(page).to have_text 'Please login as bob to activate your account.'
end
it 'registers the user upon submission' do
# login is already filled with 'bob'
fill_in 'password', with: 'dummy' # accepted by DummyAuthSource
click_button 'Sign in'
# landed on the 'my page'
expect(page).to have_text 'My account'
expect(page).to have_text 'Login: bob'
end
end
end
end end
end end

@ -162,8 +162,7 @@ describe UsersController, type: :controller do
mail: 'jdoe@gmail.com', mail: 'jdoe@gmail.com',
mail_notification: 'none' mail_notification: 'none'
}, },
pref: { }, pref: { }
send_information: '1'
end end
end end
@ -175,12 +174,17 @@ describe UsersController, type: :controller do
assert_equal 'jdoe', user.login assert_equal 'jdoe', user.login
assert_equal 'jdoe@gmail.com', user.mail assert_equal 'jdoe@gmail.com', user.mail
assert_equal 'none', user.mail_notification assert_equal 'none', user.mail_notification
assert user.check_password?('adminADMIN!') assert user.passwords.empty? # no password is assigned during creation
mail = ActionMailer::Base.deliveries.last mail = ActionMailer::Base.deliveries.last
refute_nil mail refute_nil mail
assert_equal [user.mail], mail.to assert_equal [user.mail], mail.to
assert mail.body.encoded.include?('adminADMIN!')
activation_link = Regexp.new(
"http://#{Setting.host_name}/account/activate\\?token=[a-f0-9]+",
Regexp::MULTILINE)
assert(mail.body.encoded =~ activation_link)
end end
it 'should create with failure' do it 'should create with failure' do

@ -55,5 +55,45 @@ describe OpenProject::Notifications do
OpenProject::Notifications.subscribe('notifications_spec_send') OpenProject::Notifications.subscribe('notifications_spec_send')
}.to raise_error ArgumentError, /provide a block as a callback/ }.to raise_error ArgumentError, /provide a block as a callback/
end end
describe 'clear_subscriptions:' do
let(:key) { 'test_clear_subs' }
let(:as) { [] }
let(:bs) { [] }
def example_with(clear:)
OpenProject::Notifications.subscribe(key) do |out|
as << out
end
OpenProject::Notifications.send(key, 1)
OpenProject::Notifications.subscribe(key, clear_subscriptions: clear) do |out|
bs << out
end
OpenProject::Notifications.send(key, 2)
end
context 'true' do
before do
example_with clear: true
end
it 'clears previous subscriptions' do
expect(as).to eq [1]
expect(bs).to eq [2]
end
end
context 'false' do
before do
example_with clear: false
end
it 'notifies both subscriptions' do
expect(as).to eq [1, 2]
expect(bs).to eq [2]
end
end
end
end end
end end

@ -0,0 +1,128 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'support/pages/page'
module Pages
class Groups < Page
def path
'/admin/groups'
end
def edit_group!(group_name)
click_on group_name
end
def add_user_to_group!(user_name, group_name)
visit_page unless current_page?
edit_group! group_name
group(group_name).add_user! user_name
end
def delete_group!(name)
find_group(name).find('a[data-method=delete]').click
accept_alert_dialog!
end
def find_group(name)
find('tr', text: name)
end
def has_group?(name)
has_selector? 'tr', text: name
end
def group(group_name)
Group.new group_name
end
end
class Group < Pages::Page
attr_reader :id
def initialize(id)
@id = id
end
def path
"/admin/groups/#{id}/edit"
end
def open_users_tab!
click_on 'tab-users'
end
def open_projects_tab!
click_on 'tab-memberships'
end
def add_to_project!(project_name, as:)
open_projects_tab!
select_project! project_name
Array(as).each { |role| check role }
click_on 'Add'
end
def remove_from_project!(name)
open_projects_tab!
find_project(name).find('a[data-method=delete]').click
end
def find_project(name)
find('tr', text: name)
end
def has_project?(name)
has_selector? 'tr', text: name
end
def select_project!(project_name)
select(project_name, from: 'membership_project_id')
end
def add_user!(user_name)
open_users_tab!
check user_name
click_on 'Add'
end
def remove_user!(user_name)
open_users_tab!
find_user(user_name).find('a[data-method=delete]').click
end
def find_user(user_name)
find('tr', text: user_name)
end
def has_user?(user_name)
has_selector? 'tr', text: user_name
end
end
end

@ -0,0 +1,143 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
require 'support/pages/page'
module Pages
class Members < Page
include Capybara::Select2
attr_reader :project_identifier
def initialize(project_identifier)
@project_identifier = project_identifier
end
def path
"/projects/#{project_identifier}/members"
end
def open_new_member!
click_on 'Add Member'
end
##
# Adds the given user to this project.
#
# @param user_name [String] The full name of the user.
# @param as [String] The role as which the user should be added.
def add_user!(user_name, as:)
click_on 'Add Member'
select_principal! user_name if user_name
select_role! as if as
click_on 'Add'
end
def remove_user!(user_name)
find_user(user_name).find('a[data-method=delete]').click
end
def has_added_user?(name)
has_text? "Added #{name} to the project" and
has_css? 'tr', text: name
end
##
# Checks if the members page lists the given user.
#
# @param name [String] The full name of the user.
# @param roles [Array] Checks if the user has the given role.
# @param group_membership [Boolean] True if the member is added through a group.
# Such members cannot be removed separately which
# is why there must be only an edit and no delete button.
def has_user?(name, roles: nil, group_membership: nil)
has_selector?('tr', text: name) &&
(roles.nil? || has_roles?(name, roles)) &&
(group_membership.nil? || group_membership == has_group_membership?(name))
end
def find_user(name)
find('tr', text: name)
end
def edit_user!(name, add_roles: [], remove_roles: [])
user = find_user(name)
user.find('a[title=Edit]').click
Array(add_roles).each { |role| check role }
Array(remove_roles).each { |role| uncheck role }
click_on 'Change'
end
def has_group_membership?(user_name)
user = find_user(user_name)
user.has_selector?('a[title=Edit]') &&
user.has_no_selector?('a[title=Delete]')
end
def has_roles?(user_name, roles)
user = find_user(user_name)
Array(roles).all? { |role| user.has_text? role }
end
def select_principal!(principal_name)
if !User.current.impaired?
select2(principal_name, css: '#s2id_member_user_ids')
else
find('form .principals').check principal_name
end
end
def select_role!(role_name)
if !User.current.impaired?
select2(role_name, css: '#s2id_member_role_ids')
else
find('form .roles').check role_name
end
end
def enter_principal_search!(principal_name)
if !User.current.impaired?
find('#s2id_member_user_ids')
.find('.select2-choices .select2-input')
.set(principal_name)
else
fill_in 'principal_search', with: principal_name
end
end
def go_to_page!(number)
find('.pagination a', text: number.to_s).click
end
end
end

@ -0,0 +1,80 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 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-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 doc/COPYRIGHT.rdoc for more details.
#++
module Pages
class Page
include Capybara::DSL
include RSpec::Matchers
def current_page?
URI.parse(current_url).path == path
end
def visit!
raise 'No path defined' unless path
visit path
self
end
def accept_alert_dialog!
alert_dialog.accept if selenium_driver?
end
def dismiss_alert_dialog!
alert_dialog.dismiss if selenium_driver?
end
def alert_dialog
page.driver.browser.switch_to.alert
end
def has_alert_dialog?
if selenium_driver?
begin
page.driver.browser.switch_to.alert
rescue Selenium::WebDriver::Error::NoAlertPresentError
false
end
end
end
def selenium_driver?
Capybara.current_driver.to_s.include?('selenium')
end
def set_items_per_page!(n)
Setting.per_page_options = "#{n}, 50, 100"
end
def path
nil
end
end
end
Loading…
Cancel
Save